diff --git a/F-Droid/AndroidManifest.xml b/F-Droid/AndroidManifest.xml index 9bcc88614..539870340 100644 --- a/F-Droid/AndroidManifest.xml +++ b/F-Droid/AndroidManifest.xml @@ -462,7 +462,7 @@ - + diff --git a/F-Droid/src/org/fdroid/fdroid/FDroidApp.java b/F-Droid/src/org/fdroid/fdroid/FDroidApp.java index 6cb5a63ec..07082408c 100644 --- a/F-Droid/src/org/fdroid/fdroid/FDroidApp.java +++ b/F-Droid/src/org/fdroid/fdroid/FDroidApp.java @@ -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 { diff --git a/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoManager.java b/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoManager.java index 3624aef9e..e18f58e66 100644 --- a/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoManager.java +++ b/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoManager.java @@ -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"; diff --git a/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoService.java b/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoService.java deleted file mode 100644 index 223acf42a..000000000 --- a/F-Droid/src/org/fdroid/fdroid/localrepo/LocalRepoService.java +++ /dev/null @@ -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() { - @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 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; - } - } -} diff --git a/F-Droid/src/org/fdroid/fdroid/localrepo/SwapState.java b/F-Droid/src/org/fdroid/fdroid/localrepo/SwapManager.java similarity index 67% rename from F-Droid/src/org/fdroid/fdroid/localrepo/SwapState.java rename to F-Droid/src/org/fdroid/fdroid/localrepo/SwapManager.java index bea440d76..9f5e1d1d1 100644 --- a/F-Droid/src/org/fdroid/fdroid/localrepo/SwapState.java +++ b/F-Droid/src/org/fdroid/fdroid/localrepo/SwapManager.java @@ -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 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 appsToSwap; - private SwapState(@NonNull Context context, @NonNull Set appsToSwap) { + private SwapManager(@NonNull Context context, @NonNull Set 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 packages) { StringBuilder sb = new StringBuilder(); @@ -128,7 +128,7 @@ public class SwapState { } /** - * @see SwapState#deserializePackages(String) + * @see SwapManager#deserializePackages(String) */ private static Set deserializePackages(String packages) { Set 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(); } } diff --git a/F-Droid/src/org/fdroid/fdroid/localrepo/SwapService.java b/F-Droid/src/org/fdroid/fdroid/localrepo/SwapService.java new file mode 100644 index 000000000..d4cfbbec5 --- /dev/null +++ b/F-Droid/src/org/fdroid/fdroid/localrepo/SwapService.java @@ -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(); + } + }; + +} diff --git a/F-Droid/src/org/fdroid/fdroid/localrepo/type/BonjourType.java b/F-Droid/src/org/fdroid/fdroid/localrepo/type/BonjourType.java new file mode 100644 index 000000000..cfa89803d --- /dev/null +++ b/F-Droid/src/org/fdroid/fdroid/localrepo/type/BonjourType.java @@ -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 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() { + + } + +} diff --git a/F-Droid/src/org/fdroid/fdroid/localrepo/type/NfcType.java b/F-Droid/src/org/fdroid/fdroid/localrepo/type/NfcType.java new file mode 100644 index 000000000..f71130300 --- /dev/null +++ b/F-Droid/src/org/fdroid/fdroid/localrepo/type/NfcType.java @@ -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() { + + } + +} diff --git a/F-Droid/src/org/fdroid/fdroid/localrepo/type/SwapType.java b/F-Droid/src/org/fdroid/fdroid/localrepo/type/SwapType.java new file mode 100644 index 000000000..68161616a --- /dev/null +++ b/F-Droid/src/org/fdroid/fdroid/localrepo/type/SwapType.java @@ -0,0 +1,11 @@ +package org.fdroid.fdroid.localrepo.type; + +public interface SwapType { + + void start(); + + void stop(); + + void restart(); + +} diff --git a/F-Droid/src/org/fdroid/fdroid/localrepo/type/WebServerType.java b/F-Droid/src/org/fdroid/fdroid/localrepo/type/WebServerType.java new file mode 100644 index 000000000..18e501577 --- /dev/null +++ b/F-Droid/src/org/fdroid/fdroid/localrepo/type/WebServerType.java @@ -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() { + + } + +} diff --git a/F-Droid/src/org/fdroid/fdroid/localrepo/type/WifiType.java b/F-Droid/src/org/fdroid/fdroid/localrepo/type/WifiType.java new file mode 100644 index 000000000..2ba29851c --- /dev/null +++ b/F-Droid/src/org/fdroid/fdroid/localrepo/type/WifiType.java @@ -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() { + + } + +} diff --git a/F-Droid/src/org/fdroid/fdroid/net/WifiStateChangeService.java b/F-Droid/src/org/fdroid/fdroid/net/WifiStateChangeService.java index 9c2330a04..fa55b8dbe 100644 --- a/F-Droid/src/org/fdroid/fdroid/net/WifiStateChangeService.java +++ b/F-Droid/src/org/fdroid/fdroid/net/WifiStateChangeService.java @@ -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(); } } diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/ConnectSwapActivity.java b/F-Droid/src/org/fdroid/fdroid/views/swap/ConnectSwapActivity.java index 6bb1f7efd..aa682b99a 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/swap/ConnectSwapActivity.java +++ b/F-Droid/src/org/fdroid/fdroid/views/swap/ConnectSwapActivity.java @@ -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(); } diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/JoinWifiView.java b/F-Droid/src/org/fdroid/fdroid/views/swap/JoinWifiView.java index 99baf6706..8bf9bd6dc 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/swap/JoinWifiView.java +++ b/F-Droid/src/org/fdroid/fdroid/views/swap/JoinWifiView.java @@ -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; } } diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/NfcView.java b/F-Droid/src/org/fdroid/fdroid/views/swap/NfcView.java index 142218aca..5dba1c7c1 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/swap/NfcView.java +++ b/F-Droid/src/org/fdroid/fdroid/views/swap/NfcView.java @@ -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; } } diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/SelectAppsView.java b/F-Droid/src/org/fdroid/fdroid/views/swap/SelectAppsView.java index 27ac9a012..dfbf5091e 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/swap/SelectAppsView.java +++ b/F-Droid/src/org/fdroid/fdroid/views/swap/SelectAppsView.java @@ -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) { diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/StartSwapView.java b/F-Droid/src/org/fdroid/fdroid/views/swap/StartSwapView.java index 3f71317bf..f46b21eb3 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/swap/StartSwapView.java +++ b/F-Droid/src/org/fdroid/fdroid/views/swap/StartSwapView.java @@ -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; } } diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java index 5a185c672..f6d52d9ec 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java +++ b/F-Droid/src/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java @@ -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(); } diff --git a/F-Droid/src/org/fdroid/fdroid/views/swap/WifiQrView.java b/F-Droid/src/org/fdroid/fdroid/views/swap/WifiQrView.java index 8b215d505..9a0d4f449 100644 --- a/F-Droid/src/org/fdroid/fdroid/views/swap/WifiQrView.java +++ b/F-Droid/src/org/fdroid/fdroid/views/swap/WifiQrView.java @@ -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() {