WIP: Refactoring swap service.

Removed LocalRepoService, replaced with SwapService.

Still TODO:
  Manage threads. Currently everything is called from the
  UI thread, which is a regression from the previous behaviour.
  I'd like to manage this so that the code interacting with the
  SwapManager doesn't need to bother itself with whether it is calling
  from the UI thread or not.

The local repo service had many different methods and properties for
dealing with starting and stopping various things (webserver, bonjour,
in the future it will also need to know about bluetooth and Wifi AP).

The SwapService handles this stuff by delegating to specific classes
that are only responsible for one of these. Hopefully this will make
the process of enabling and disabling swap repos easier to reason
about.

The local repo service was also stopped and started quite regularly.
This meant it was up to the code making use of the service to know if
it was running or not, and to enable it if required.
The new SwapService is only started once (when the singleton
SwapManager is created for the first time). It should not use any more
resources, because it is a background service most the time, and it
is responsible for moving itself to the foreground when required (the
burden is not on the code consuming this service to know when to do
this).

By having the service running more often, it doesn't need to'
continually figure out if it  needs to register or unregister listeners
for various properties (e.g. https enabled) or wifi broadcasts. The
listeners can stay active, and do nothing once notified if swapping is
not enabled.

Moved the timeout timer (which cancels the swap service after 15 mins)
into the SwapService, rather than being managed by the
SwapWorkflowActivity. Seems more appropriate for the service to know to
time itself out rather than the Activity, seeing as the Activity can
die and get GC'ed while the service is still running.

Finally, although there is nothing stopping code in F-Droid from
talking to the service directly, it is now handled by the SwapManager
singleton. This means that details such as using a Messenger or Handler
object in order to communicate via arg1 and arg2 is no longer required,
and instead methods with proper type signatures can be used. This is
similar (but not exactly the same) to how Android system services work.
That is, ask for a "Manager" object using getSystemService(), and then
use that to perform functionality and query state via that object,
which delegates to the service. Then we get the best of both worlds:

 * Reasonable and type safe method signatures
 * Services that are not tied to activity lifecycles, which persist
   beyond the closing of the swap activity.
This commit is contained in:
Peter Serwylo 2015-05-31 22:54:57 +10:00
parent 0e16eae5bc
commit 7c9492e6b4
19 changed files with 537 additions and 435 deletions

View File

@ -462,7 +462,7 @@
<service android:name=".UpdateService" />
<service android:name=".net.WifiStateChangeService" />
<service android:name=".localrepo.LocalRepoService" />
<service android:name=".localrepo.SwapService" />
</application>
</manifest>

View File

@ -23,10 +23,8 @@ import android.app.Activity;
import android.app.Application;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
@ -34,10 +32,6 @@ import android.content.pm.ResolveInfo;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Build;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.preference.PreferenceManager;
import android.widget.Toast;
@ -52,14 +46,12 @@ import org.fdroid.fdroid.compat.PRNGFixes;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.InstalledAppCacheUpdater;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.localrepo.LocalRepoService;
import org.fdroid.fdroid.net.IconDownloader;
import org.fdroid.fdroid.net.WifiStateChangeService;
import java.io.File;
import java.security.Security;
import java.util.Locale;
import java.util.Set;
public class FDroidApp extends Application {

View File

@ -55,6 +55,12 @@ import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
/**
* The {@link SwapManager} 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.
*/
public class LocalRepoManager {
private static final String TAG = "LocalRepoManager";

View File

@ -1,312 +0,0 @@
package org.fdroid.fdroid.localrepo;
import android.annotation.SuppressLint;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.Messenger;
import android.support.v4.app.NotificationCompat;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.Preferences.ChangeListener;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.net.LocalHTTPD;
import org.fdroid.fdroid.net.WifiStateChangeService;
import org.fdroid.fdroid.views.swap.SwapWorkflowActivity;
import java.io.IOException;
import java.net.BindException;
import java.util.HashMap;
import java.util.Random;
import javax.jmdns.JmDNS;
import javax.jmdns.ServiceInfo;
public class LocalRepoService extends Service {
private static final String TAG = "LocalRepoService";
public static final String STATE = "org.fdroid.fdroid.action.LOCAL_REPO_STATE";
public static final String STARTED = "org.fdroid.fdroid.category.LOCAL_REPO_STARTED";
public static final String STOPPED = "org.fdroid.fdroid.category.LOCAL_REPO_STOPPED";
private NotificationManager notificationManager;
private Notification notification;
// Unique Identification Number for the Notification.
// We use it on Notification start, and to cancel it.
private final int NOTIFICATION = R.string.local_repo_running;
private Handler webServerThreadHandler = null;
private LocalHTTPD localHttpd;
private JmDNS jmdns;
private ServiceInfo pairService;
public static final int START = 1111111;
public static final int STOP = 12345678;
public static final int RESTART = 87654;
final Messenger messenger = new Messenger(new StartStopHandler(this));
/**
* This is most likely going to be created on the UI thread, hence all of
* the message handling will take place on a new thread to prevent blocking
* the UI.
*/
static class StartStopHandler extends Handler {
private final LocalRepoService service;
public StartStopHandler(LocalRepoService service) {
this.service = service;
}
@Override
public void handleMessage(final Message msg) {
new Thread() {
public void run() {
switch (msg.arg1) {
case START:
service.startNetworkServices();
break;
case STOP:
service.stopNetworkServices();
break;
case RESTART:
service.stopNetworkServices();
service.startNetworkServices();
break;
default:
Log.e(TAG, "Unsupported msg.arg1 (" + msg.arg1 + "), ignored");
break;
}
}
}.start();
}
}
private final BroadcastReceiver onWifiChange = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent i) {
stopNetworkServices();
startNetworkServices();
}
};
private ChangeListener localRepoBonjourChangeListener = new ChangeListener() {
@Override
public void onPreferenceChange() {
if (localHttpd.isAlive())
if (Preferences.get().isLocalRepoBonjourEnabled())
registerMDNSService();
else
unregisterMDNSService();
}
};
private final ChangeListener localRepoHttpsChangeListener = new ChangeListener() {
@Override
public void onPreferenceChange() {
Log.i(TAG, "onPreferenceChange");
if (localHttpd.isAlive()) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
stopNetworkServices();
startNetworkServices();
return null;
}
}.execute();
}
}
};
private void showNotification() {
notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
// launch LocalRepoActivity if the user selects this notification
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);
notification = 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();
startForeground(NOTIFICATION, notification);
}
@Override
public void onCreate() {
showNotification();
startNetworkServices();
Preferences.get().registerLocalRepoBonjourListeners(localRepoBonjourChangeListener);
LocalBroadcastManager.getInstance(this).registerReceiver(onWifiChange,
new IntentFilter(WifiStateChangeService.BROADCAST));
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// We want this service to continue running until it is explicitly
// stopped, so return sticky.
return START_STICKY;
}
@Override
public void onDestroy() {
new Thread() {
public void run() {
stopNetworkServices();
}
}.start();
notificationManager.cancel(NOTIFICATION);
LocalBroadcastManager.getInstance(this).unregisterReceiver(onWifiChange);
Preferences.get().unregisterLocalRepoBonjourListeners(localRepoBonjourChangeListener);
}
@Override
public IBinder onBind(Intent intent) {
return messenger.getBinder();
}
private void startNetworkServices() {
Log.d(TAG, "Starting local repo network services");
startWebServer();
if (Preferences.get().isLocalRepoBonjourEnabled())
registerMDNSService();
Preferences.get().registerLocalRepoHttpsListeners(localRepoHttpsChangeListener);
}
private void stopNetworkServices() {
Log.d(TAG, "Stopping local repo network services");
Preferences.get().unregisterLocalRepoHttpsListeners(localRepoHttpsChangeListener);
Log.d(TAG, "Unregistering MDNS service...");
unregisterMDNSService();
Log.d(TAG, "Stopping web server...");
stopWebServer();
}
private void startWebServer() {
Runnable webServer = new Runnable() {
// Tell Eclipse this is not a leak because of Looper use.
@SuppressLint("HandlerLeak")
@Override
public void run() {
localHttpd = new LocalHTTPD(
LocalRepoService.this,
getFilesDir(),
Preferences.get().isLocalRepoHttpsEnabled());
Looper.prepare(); // must be run before creating a Handler
webServerThreadHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
Log.i(TAG, "we've been asked to stop the webserver: " + msg.obj);
localHttpd.stop();
}
};
try {
localHttpd.start();
} catch (BindException e) {
int prev = FDroidApp.port;
FDroidApp.port = FDroidApp.port + new Random().nextInt(1111);
Log.w(TAG, "port " + prev + " occupied, trying on " + FDroidApp.port + "!");
startService(new Intent(LocalRepoService.this, WifiStateChangeService.class));
} catch (IOException e) {
Log.e(TAG, "Could not start local repo HTTP server: " + e);
Log.e(TAG, Log.getStackTraceString(e));
}
Looper.loop(); // start the message receiving loop
}
};
new Thread(webServer).start();
Intent intent = new Intent(STATE);
intent.putExtra(STATE, STARTED);
LocalBroadcastManager.getInstance(LocalRepoService.this).sendBroadcast(intent);
}
private void stopWebServer() {
if (webServerThreadHandler == null) {
Log.i(TAG, "null handler in stopWebServer");
return;
}
Message msg = webServerThreadHandler.obtainMessage();
msg.obj = webServerThreadHandler.getLooper().getThread().getName() + " says stop";
webServerThreadHandler.sendMessage(msg);
Intent intent = new Intent(STATE);
intent.putExtra(STATE, STOPPED);
LocalBroadcastManager.getInstance(LocalRepoService.this).sendBroadcast(intent);
}
private void registerMDNSService() {
new Thread(new Runnable() {
@Override
public void run() {
/*
* a ServiceInfo can only be registered with a single instance
* of JmDNS, and there is only ever a single LocalHTTPD port to
* advertise anyway.
*/
if (pairService != null || jmdns != null)
clearCurrentMDNSService();
String repoName = Preferences.get().getLocalRepoName();
HashMap<String, String> values = new HashMap<>();
values.put("path", "/fdroid/repo");
values.put("name", repoName);
values.put("fingerprint", FDroidApp.repo.fingerprint);
String type;
if (Preferences.get().isLocalRepoHttpsEnabled()) {
values.put("type", "fdroidrepos");
type = "_https._tcp.local.";
} else {
values.put("type", "fdroidrepo");
type = "_http._tcp.local.";
}
try {
pairService = ServiceInfo.create(type, repoName, FDroidApp.port, 0, 0, values);
jmdns = JmDNS.create();
jmdns.registerService(pairService);
} catch (IOException e) {
Log.e(TAG, "Error while registering jmdns service: " + e);
Log.e(TAG, Log.getStackTraceString(e));
}
}
}).start();
}
private void unregisterMDNSService() {
if (localRepoBonjourChangeListener != null) {
Preferences.get().unregisterLocalRepoBonjourListeners(localRepoBonjourChangeListener);
localRepoBonjourChangeListener = null;
}
clearCurrentMDNSService();
}
private void clearCurrentMDNSService() {
if (jmdns != null) {
if (pairService != null) {
jmdns.unregisterService(pairService);
pairService = null;
}
jmdns.unregisterAllServices();
Utils.closeQuietly(jmdns);
jmdns = null;
}
}
}

View File

@ -6,11 +6,10 @@ import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@ -18,21 +17,20 @@ import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
public class SwapState {
public class SwapManager {
private static final String TAG = "SwapState";
private static final String SHARED_PREFERENCES = "swap-state";
private static final String KEY_STEP = "step";
private static final String KEY_APPS_TO_SWAP = "appsToSwap";
private static SwapState instance;
private static SwapManager instance;
@NonNull
public static SwapState load(@NonNull Context context) {
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 SwapState(context, appsToSwap);
instance = new SwapManager(context, appsToSwap);
}
return instance;
@ -44,9 +42,11 @@ public class SwapState {
@NonNull
private Set<String> appsToSwap;
private SwapState(@NonNull Context context, @NonNull Set<String> appsToSwap) {
private SwapManager(@NonNull Context context, @NonNull Set<String> appsToSwap) {
this.context = context.getApplicationContext();
this.appsToSwap = appsToSwap;
setupService();
}
/**
@ -81,7 +81,7 @@ public class SwapState {
return step;
}
public SwapState setStep(@SwapStep int step) {
public SwapManager setStep(@SwapStep int step) {
this.step = step;
return this;
}
@ -114,7 +114,7 @@ public class SwapState {
* 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 SwapState#deserializePackages(String)
* @see SwapManager#deserializePackages(String)
*/
private static String serializePackages(Set<String> packages) {
StringBuilder sb = new StringBuilder();
@ -128,7 +128,7 @@ public class SwapState {
}
/**
* @see SwapState#deserializePackages(String)
* @see SwapManager#deserializePackages(String)
*/
private static Set<String> deserializePackages(String packages) {
Set<String> set = new HashSet<>();
@ -164,53 +164,61 @@ public class SwapState {
// Local repo stop/start/restart handling
// ==========================================
private Messenger localRepoServiceMessenger = null;
private boolean localRepoServiceIsBound = false;
@Nullable
private SwapService service = null;
private final ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
localRepoServiceMessenger = new Messenger(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();
}
@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);
}
@Override
public void onServiceDisconnected(ComponentName className) {
localRepoServiceMessenger = null;
}
};
}
public void startLocalRepoService() {
if (!localRepoServiceIsBound) {
Intent service = new Intent(context, LocalRepoService.class);
localRepoServiceIsBound = context.bindService(service, serviceConnection, Context.BIND_AUTO_CREATE);
if (localRepoServiceIsBound)
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 stopLocalRepoService() {
if (localRepoServiceIsBound) {
context.unbindService(serviceConnection);
localRepoServiceIsBound = false;
public void disableSwapping() {
if (service != null) {
service.disableSwapping();
} else {
Log.e(TAG, "Couldn't disable swap, because service was not running.");
}
context.stopService(new Intent(context, LocalRepoService.class));
}
/**
* Handles checking if the {@link LocalRepoService} is running, and only restarts it if it was running.
* Handles checking if the {@link SwapService} is running, and only restarts it if it was running.
*/
public void restartLocalRepoServiceIfRunning() {
if (localRepoServiceMessenger != null) {
try {
Message msg = Message.obtain(null, LocalRepoService.RESTART, LocalRepoService.RESTART, 0);
localRepoServiceMessenger.send(msg);
} catch (RemoteException e) {
e.printStackTrace();
}
public void restartIfEnabled() {
if (service != null) {
service.restartIfEnabled();
}
}
public boolean isLocalRepoServiceRunning() {
return localRepoServiceIsBound;
public boolean isEnabled() {
return service != null && service.isEnabled();
}
}

View File

@ -0,0 +1,187 @@
package org.fdroid.fdroid.localrepo;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.IBinder;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.localrepo.type.BonjourType;
import org.fdroid.fdroid.localrepo.type.NfcType;
import org.fdroid.fdroid.localrepo.type.WebServerType;
import org.fdroid.fdroid.net.WifiStateChangeService;
import org.fdroid.fdroid.views.swap.SwapWorkflowActivity;
import java.util.Timer;
import java.util.TimerTask;
/**
* Central service which manages all of the different moving parts of swap which are required
* to enable p2p swapping of apps. Currently manages WiFi and NFC. Will manage Bluetooth in
* the future.
*
* TODO: Manage threading correctly.
*/
public class SwapService extends Service {
private static final String TAG = "SwapService";
private static final int NOTIFICATION = 1;
private final Binder binder = new Binder();
private final BonjourType bonjourType;
private final WebServerType webServerType;
// TODO: The NFC type can't really be managed by the service, because it is intrinsically tied
// to a specific _Activity_, and will only be active while that activity is shown. This service
// knows nothing about activities.
private final NfcType nfcType;
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 class Binder extends android.os.Binder {
public SwapService getService() {
return SwapService.this;
}
}
public SwapService() {
nfcType = new NfcType(this);
bonjourType = new BonjourType(this);
webServerType = new WebServerType(this);
}
public void onCreate() {
super.onCreate();
Preferences.get().unregisterLocalRepoBonjourListeners(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();
disableSwapping();
}
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;
public void enableSwapping() {
if (!enabled) {
nfcType.start();
webServerType.start();
bonjourType.start();
startForeground(NOTIFICATION, createNotification());
enabled = true;
}
// Regardless of whether it was previously enabled, start the timer again. This ensures that
// if, e.g. a person views the swap activity again, it will attempt to enable swapping if
// appropriate, and thus restart this timer.
initTimer();
}
public void disableSwapping() {
if (enabled) {
bonjourType.stop();
webServerType.stop();
nfcType.stop();
stopForeground(true);
if (timer != null) {
timer.cancel();
}
enabled = false;
}
}
public boolean isEnabled() {
return enabled;
}
public void restartIfEnabled() {
if (enabled) {
disableSwapping();
enableSwapping();
}
}
private void initTimer() {
if (timer != null)
timer.cancel();
// automatically turn off after 15 minutes
timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
disableSwapping();
}
}, TIMEOUT);
}
@SuppressWarnings("FieldCanBeLocal") // The constructor will get bloated if these are all local...
private final Preferences.ChangeListener bonjourEnabledListener = new Preferences.ChangeListener() {
@Override
public void onPreferenceChange() {
Log.i(TAG, "Use Bonjour while swapping preference changed.");
if (enabled)
if (Preferences.get().isLocalRepoBonjourEnabled())
bonjourType.start();
else
bonjourType.stop();
}
};
@SuppressWarnings("FieldCanBeLocal") // The constructor will get bloated if these are all local...
private final Preferences.ChangeListener httpsEnabledListener = new Preferences.ChangeListener() {
@Override
public void onPreferenceChange() {
Log.i(TAG, "Swap over HTTPS preference changed.");
restartIfEnabled();
}
};
@SuppressWarnings("FieldCanBeLocal") // The constructor will get bloated if these are all local...
private final BroadcastReceiver onWifiChange = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent i) {
restartIfEnabled();
}
};
}

View File

@ -0,0 +1,87 @@
package org.fdroid.fdroid.localrepo.type;
import android.content.Context;
import android.util.Log;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.Utils;
import java.io.IOException;
import java.util.HashMap;
import javax.jmdns.JmDNS;
import javax.jmdns.ServiceInfo;
public class BonjourType implements SwapType {
private static final String TAG = "BonjourType";
private JmDNS jmdns;
private ServiceInfo pairService;
private final Context context;
public BonjourType(Context context) {
this.context = context;
}
@Override
public void start() {
if (Preferences.get().isLocalRepoBonjourEnabled())
return;
/*
* a ServiceInfo can only be registered with a single instance
* of JmDNS, and there is only ever a single LocalHTTPD port to
* advertise anyway.
*/
if (pairService != null || jmdns != null)
clearCurrentMDNSService();
String repoName = Preferences.get().getLocalRepoName();
HashMap<String, String> values = new HashMap<>();
values.put("path", "/fdroid/repo");
values.put("name", repoName);
values.put("fingerprint", FDroidApp.repo.fingerprint);
String type;
if (Preferences.get().isLocalRepoHttpsEnabled()) {
values.put("type", "fdroidrepos");
type = "_https._tcp.local.";
} else {
values.put("type", "fdroidrepo");
type = "_http._tcp.local.";
}
try {
pairService = ServiceInfo.create(type, repoName, FDroidApp.port, 0, 0, values);
jmdns = JmDNS.create();
jmdns.registerService(pairService);
} catch (IOException e) {
Log.e(TAG, "Error while registering jmdns service: " + e);
Log.e(TAG, Log.getStackTraceString(e));
}
}
@Override
public void stop() {
Log.d(TAG, "Unregistering MDNS service...");
clearCurrentMDNSService();
}
private void clearCurrentMDNSService() {
if (jmdns != null) {
if (pairService != null) {
jmdns.unregisterService(pairService);
pairService = null;
}
jmdns.unregisterAllServices();
Utils.closeQuietly(jmdns);
jmdns = null;
}
}
@Override
public void restart() {
}
}

View File

@ -0,0 +1,27 @@
package org.fdroid.fdroid.localrepo.type;
import android.content.Context;
public class NfcType implements SwapType {
private final Context context;
public NfcType(Context context) {
this.context = context;
}
@Override
public void start() {
}
@Override
public void stop() {
}
@Override
public void restart() {
}
}

View File

@ -0,0 +1,11 @@
package org.fdroid.fdroid.localrepo.type;
public interface SwapType {
void start();
void stop();
void restart();
}

View File

@ -0,0 +1,96 @@
package org.fdroid.fdroid.localrepo.type;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.net.LocalHTTPD;
import org.fdroid.fdroid.net.WifiStateChangeService;
import java.io.IOException;
import java.net.BindException;
import java.util.Random;
public class WebServerType implements SwapType {
private static final String TAG = "WebServerType";
private Handler webServerThreadHandler = null;
private LocalHTTPD localHttpd;
private final Context context;
public WebServerType(Context context) {
this.context = context;
}
@Override
public void start() {
Runnable webServer = new Runnable() {
// Tell Eclipse this is not a leak because of Looper use.
@SuppressLint("HandlerLeak")
@Override
public void run() {
localHttpd = new LocalHTTPD(
context,
context.getFilesDir(),
Preferences.get().isLocalRepoHttpsEnabled());
Looper.prepare(); // must be run before creating a Handler
webServerThreadHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
Log.i(TAG, "we've been asked to stop the webserver: " + msg.obj);
localHttpd.stop();
}
};
try {
localHttpd.start();
} catch (BindException e) {
int prev = FDroidApp.port;
FDroidApp.port = FDroidApp.port + new Random().nextInt(1111);
Log.w(TAG, "port " + prev + " occupied, trying on " + FDroidApp.port + "!");
context.startService(new Intent(context, WifiStateChangeService.class));
} catch (IOException e) {
Log.e(TAG, "Could not start local repo HTTP server: " + e);
Log.e(TAG, Log.getStackTraceString(e));
}
Looper.loop(); // start the message receiving loop
}
};
new Thread(webServer).start();
// TODO: Don't think these were ever being received by anyone...
/*Intent intent = new Intent(STATE);
intent.putExtra(STATE, STARTED);
LocalBroadcastManager.getInstance(LocalRepoService.this).sendBroadcast(intent);*/
}
@Override
public void stop() {
if (webServerThreadHandler == null) {
Log.i(TAG, "null handler in stopWebServer");
return;
}
Message msg = webServerThreadHandler.obtainMessage();
msg.obj = webServerThreadHandler.getLooper().getThread().getName() + " says stop";
webServerThreadHandler.sendMessage(msg);
// TODO: Don't think these were ever being received by anyone...
/*Intent intent = new Intent(STATE);
intent.putExtra(STATE, STOPPED);
LocalBroadcastManager.getInstance(LocalRepoService.this).sendBroadcast(intent);*/
}
@Override
public void restart() {
}
}

View File

@ -0,0 +1,28 @@
package org.fdroid.fdroid.localrepo.type;
import android.content.Context;
public class WifiType implements SwapType {
private final Context context;
public WifiType(Context context) {
this.context = context;
}
@Override
public void start() {
}
@Override
public void stop() {
}
@Override
public void restart() {
}
}

View File

@ -17,7 +17,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.SwapState;
import org.fdroid.fdroid.localrepo.SwapManager;
import java.net.Inet6Address;
import java.net.InetAddress;
@ -152,7 +152,7 @@ public class WifiStateChangeService extends Service {
Intent intent = new Intent(BROADCAST);
LocalBroadcastManager.getInstance(WifiStateChangeService.this).sendBroadcast(intent);
WifiStateChangeService.this.stopSelf();
SwapState.load(WifiStateChangeService.this).restartLocalRepoServiceIfRunning();
SwapManager.load(WifiStateChangeService.this).restartIfEnabled();
}
}

View File

@ -27,7 +27,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.SwapState;
import org.fdroid.fdroid.localrepo.SwapManager;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
@ -155,7 +155,7 @@ public class ConnectSwapActivity extends ActionBarActivity implements ProgressLi
// 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() && SwapState.load(this).isLocalRepoServiceRunning()) {
if (!newRepoConfig.preventFurtherSwaps() && SwapManager.load(this).isEnabled()) {
askServerToSwapWithUs();
}

View File

@ -22,9 +22,8 @@ import android.widget.TextView;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.localrepo.SwapState;
import org.fdroid.fdroid.localrepo.SwapManager;
import org.fdroid.fdroid.net.WifiStateChangeService;
import org.fdroid.fdroid.views.swap.SwapWorkflowActivity;
public class JoinWifiView extends RelativeLayout implements SwapWorkflowActivity.InnerView {
@ -122,11 +121,11 @@ public class JoinWifiView extends RelativeLayout implements SwapWorkflowActivity
@Override
public int getStep() {
return SwapState.STEP_JOIN_WIFI;
return SwapManager.STEP_JOIN_WIFI;
}
@Override
public int getPreviousStep() {
return SwapState.STEP_SELECT_APPS;
return SwapManager.STEP_SELECT_APPS;
}
}

View File

@ -15,8 +15,7 @@ import android.widget.RelativeLayout;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.localrepo.SwapState;
import org.fdroid.fdroid.views.swap.SwapWorkflowActivity;
import org.fdroid.fdroid.localrepo.SwapManager;
public class NfcView extends RelativeLayout implements SwapWorkflowActivity.InnerView {
@ -70,11 +69,11 @@ public class NfcView extends RelativeLayout implements SwapWorkflowActivity.Inne
@Override
public int getStep() {
return SwapState.STEP_SHOW_NFC;
return SwapManager.STEP_SHOW_NFC;
}
@Override
public int getPreviousStep() {
return SwapState.STEP_JOIN_WIFI;
return SwapManager.STEP_JOIN_WIFI;
}
}

View File

@ -34,7 +34,7 @@ import android.widget.TextView;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.InstalledAppProvider;
import org.fdroid.fdroid.localrepo.SwapState;
import org.fdroid.fdroid.localrepo.SwapManager;
public class SelectAppsView extends ListView implements
SwapWorkflowActivity.InnerView,
@ -62,7 +62,7 @@ public class SelectAppsView extends ListView implements
return (SwapWorkflowActivity)getContext();
}
private SwapState getState() {
private SwapManager getState() {
return getActivity().getState();
}
@ -136,12 +136,12 @@ public class SelectAppsView extends ListView implements
@Override
public int getStep() {
return SwapState.STEP_SELECT_APPS;
return SwapManager.STEP_SELECT_APPS;
}
@Override
public int getPreviousStep() {
return SwapState.STEP_INTRO;
return SwapManager.STEP_INTRO;
}
private void toggleAppSelected(int position) {

View File

@ -5,15 +5,13 @@ import android.content.Context;
import android.os.Build;
import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.view.ContextThemeWrapper;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.widget.LinearLayout;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.localrepo.SwapState;
import org.fdroid.fdroid.views.swap.SwapWorkflowActivity;
import org.fdroid.fdroid.localrepo.SwapManager;
public class StartSwapView extends LinearLayout implements SwapWorkflowActivity.InnerView {
@ -66,7 +64,7 @@ public class StartSwapView extends LinearLayout implements SwapWorkflowActivity.
@Override
public int getStep() {
return SwapState.STEP_INTRO;
return SwapManager.STEP_INTRO;
}
@Override
@ -75,6 +73,6 @@ public class StartSwapView extends LinearLayout implements SwapWorkflowActivity.
// 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 SwapState.STEP_INTRO;
return SwapManager.STEP_INTRO;
}
}

View File

@ -10,7 +10,6 @@ import android.os.Bundle;
import android.support.annotation.LayoutRes;
import android.support.annotation.NonNull;
import android.support.v4.app.FragmentActivity;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@ -29,37 +28,40 @@ 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.SwapState;
import org.fdroid.fdroid.localrepo.SwapManager;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
public class SwapWorkflowActivity extends FragmentActivity {
private ViewGroup container;
/**
* 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
* the previous view before them should be.
*/
public interface InnerView {
/** @return True if the menu should be shown. */
boolean buildMenu(Menu menu, @NonNull MenuInflater inflater);
/** @return The step that this view represents. */
@SwapState.SwapStep int getStep();
@SwapManager.SwapStep int getStep();
@SwapState.SwapStep int getPreviousStep();
@SwapManager.SwapStep int getPreviousStep();
}
private static final int CONNECT_TO_SWAP = 1;
private SwapState state;
private SwapManager state;
private InnerView currentView;
private boolean hasPreparedLocalRepo = false;
private UpdateAsyncTask updateSwappableAppsTask = null;
private Timer shutdownLocalRepoTimer;
@Override
public void onBackPressed() {
if (currentView.getStep() == SwapState.STEP_INTRO) {
if (currentView.getStep() == SwapManager.STEP_INTRO) {
finish();
} else {
int nextStep = currentView.getPreviousStep();
@ -71,7 +73,7 @@ public class SwapWorkflowActivity extends FragmentActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
state = SwapState.load(this);
state = SwapManager.load(this);
setContentView(R.layout.swap_activity);
container = (ViewGroup) findViewById(R.id.fragment_container);
showRelevantView();
@ -98,25 +100,25 @@ public class SwapWorkflowActivity extends FragmentActivity {
}
switch(state.getStep()) {
case SwapState.STEP_INTRO:
case SwapManager.STEP_INTRO:
showIntro();
break;
case SwapState.STEP_SELECT_APPS:
case SwapManager.STEP_SELECT_APPS:
showSelectApps();
break;
case SwapState.STEP_SHOW_NFC:
case SwapManager.STEP_SHOW_NFC:
showNfc();
break;
case SwapState.STEP_JOIN_WIFI:
case SwapManager.STEP_JOIN_WIFI:
showJoinWifi();
break;
case SwapState.STEP_WIFI_QR:
case SwapManager.STEP_WIFI_QR:
showWifiQr();
break;
}
}
public SwapState getState() {
public SwapManager getState() {
return state;
}
@ -197,36 +199,11 @@ public class SwapWorkflowActivity extends FragmentActivity {
}
private void ensureLocalRepoRunning() {
if (!getState().isLocalRepoServiceRunning()) {
getState().startLocalRepoService();
initLocalRepoTimer(900000); // 15 mins
}
}
private void initLocalRepoTimer(long timeoutMilliseconds) {
// reset the timer if viewing this Activity again
if (shutdownLocalRepoTimer != null)
shutdownLocalRepoTimer.cancel();
// automatically turn off after 15 minutes
shutdownLocalRepoTimer = new Timer();
shutdownLocalRepoTimer.schedule(new TimerTask() {
@Override
public void run() {
getState().stopLocalRepoService();
}
}, timeoutMilliseconds);
getState().enableSwapping();
}
public void stopSwapping() {
if (getState().isLocalRepoServiceRunning()) {
if (shutdownLocalRepoTimer != null) {
shutdownLocalRepoTimer.cancel();
}
getState().stopLocalRepoService();
}
getState().disableSwapping();
finish();
}

View File

@ -30,9 +30,8 @@ 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.SwapState;
import org.fdroid.fdroid.localrepo.SwapManager;
import org.fdroid.fdroid.net.WifiStateChangeService;
import org.fdroid.fdroid.views.swap.SwapWorkflowActivity;
import java.net.URI;
import java.util.List;
@ -114,13 +113,13 @@ public class WifiQrView extends ScrollView implements SwapWorkflowActivity.Inner
@Override
public int getStep() {
return SwapState.STEP_WIFI_QR;
return SwapManager.STEP_WIFI_QR;
}
@Override
public int getPreviousStep() {
// TODO: Find a way to make this optionally go back to the NFC screen if appropriate.
return SwapState.STEP_JOIN_WIFI;
return SwapManager.STEP_JOIN_WIFI;
}
private void setUIFromWifi() {