WIP: Bluetooth communication between devices is up and running (not finished).

Devices now make themselves discoverable, and the client sends a test ping.
They UI is not styles properly though, and it doesn't handle the case where
somebody chooses to make their device not-discoverable (because the desired
peer is already paired and it is unneccesary). It also doesn't handle failure
anywhere.
This commit is contained in:
Peter Serwylo 2014-10-14 07:02:19 +10:30 committed by Peter Serwylo
parent fba02e32b5
commit 7dff9a9499
21 changed files with 832 additions and 281 deletions

View File

@ -456,7 +456,8 @@
<service android:name=".UpdateService" /> <service android:name=".UpdateService" />
<service android:name=".net.WifiStateChangeService" /> <service android:name=".net.WifiStateChangeService" />
<service android:name=".localrepo.LocalRepoService" /> <service android:name=".localrepo.LocalRepoWifiService" />
<service android:name=".localrepo.LocalRepoProxyService" />
</application> </application>
</manifest> </manifest>

View File

@ -43,15 +43,20 @@
<item name="android:background">@color/white</item> <item name="android:background">@color/white</item>
</style> </style>
<style name="SwapTheme.BluetoothDeviceList" parent="AppThemeLightWithDarkActionBar"> <style name="SwapTheme.StartSwap.Text" parent="@style/SwapTheme.StartSwap">
</style>
<style name="SwapTheme.BluetoothDeviceList" parent="@style/SwapTheme.StartSwap">
</style> </style>
<style name="SwapTheme.BluetoothDeviceList.ListItem" parent="AppThemeLightWithDarkActionBar"> <style name="SwapTheme.BluetoothDeviceList.ListItem" parent="AppThemeLightWithDarkActionBar">
</style> </style>
<style name="SwapTheme.BluetoothDeviceList.Heading" parent="@style/SwapTheme.Wizard.MainText"> <style name="SwapTheme.BluetoothDeviceList.Text" parent="@style/SwapTheme.BluetoothDeviceList">
</style>
<style name="SwapTheme.BluetoothDeviceList.Heading" parent="@style/SwapTheme.BluetoothDeviceList.Text">
<item name="android:textSize">32.5dp</item> <!-- 58px * 96dpi / 160dpi = 32.5sp --> <item name="android:textSize">32.5dp</item> <!-- 58px * 96dpi / 160dpi = 32.5sp -->
<item name="android:textColor">#222</item>
</style> </style>
<style name="SwapTheme.AppList" parent="AppThemeLightWithDarkActionBar"> <style name="SwapTheme.AppList" parent="AppThemeLightWithDarkActionBar">

View File

@ -52,7 +52,9 @@ import org.fdroid.fdroid.compat.PRNGFixes;
import org.fdroid.fdroid.data.AppProvider; import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.InstalledAppCacheUpdater; import org.fdroid.fdroid.data.InstalledAppCacheUpdater;
import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.localrepo.LocalRepoProxyService;
import org.fdroid.fdroid.localrepo.LocalRepoService; import org.fdroid.fdroid.localrepo.LocalRepoService;
import org.fdroid.fdroid.localrepo.LocalRepoWifiService;
import org.fdroid.fdroid.net.IconDownloader; import org.fdroid.fdroid.net.IconDownloader;
import org.fdroid.fdroid.net.WifiStateChangeService; import org.fdroid.fdroid.net.WifiStateChangeService;
@ -73,8 +75,6 @@ public class FDroidApp extends Application {
// Leaving the fully qualified class name here to help clarify the difference between spongy/bouncy castle. // Leaving the fully qualified class name here to help clarify the difference between spongy/bouncy castle.
private static final org.spongycastle.jce.provider.BouncyCastleProvider spongyCastleProvider; private static final org.spongycastle.jce.provider.BouncyCastleProvider spongyCastleProvider;
private static Messenger localRepoServiceMessenger = null;
private static boolean localRepoServiceIsBound = false;
private static final String TAG = "FDroidApp"; private static final String TAG = "FDroidApp";
@ -91,6 +91,9 @@ public class FDroidApp extends Application {
private static Theme curTheme = Theme.dark; private static Theme curTheme = Theme.dark;
public static final LocalRepoServiceManager localRepoWifi = new LocalRepoServiceManager(LocalRepoWifiService.class);
public static final LocalRepoServiceManager localRepoProxy = new LocalRepoServiceManager(LocalRepoProxyService.class);
public void reloadTheme() { public void reloadTheme() {
curTheme = Theme.valueOf(PreferenceManager curTheme = Theme.valueOf(PreferenceManager
.getDefaultSharedPreferences(getBaseContext()) .getDefaultSharedPreferences(getBaseContext())
@ -302,7 +305,25 @@ public class FDroidApp extends Application {
} }
} }
private static final ServiceConnection serviceConnection = new ServiceConnection() { /**
* Helper class to encapsulate functionality relating to local repo service starting/stopping/restarting.
* It used to live as static methods in FDroidApp, but once there were two types of local repos which
* could get started (wifi and local proxy for bluetooth) then it got a bit messy. This allows us to
* support managing both of these services through two static variables
* {@link org.fdroid.fdroid.FDroidApp#localRepoProxy} and {@link org.fdroid.fdroid.FDroidApp#localRepoWifi}.
*/
public static class LocalRepoServiceManager {
private Messenger localRepoServiceMessenger = null;
private boolean localRepoServiceIsBound = false;
private final Class<?extends LocalRepoService> serviceType;
public LocalRepoServiceManager(Class<?extends LocalRepoService> serviceType) {
this.serviceType = serviceType;
}
private ServiceConnection serviceConnection = new ServiceConnection() {
@Override @Override
public void onServiceConnected(ComponentName className, IBinder service) { public void onServiceConnected(ComponentName className, IBinder service) {
localRepoServiceMessenger = new Messenger(service); localRepoServiceMessenger = new Messenger(service);
@ -314,29 +335,29 @@ public class FDroidApp extends Application {
} }
}; };
public static void startLocalRepoService(Context context) { public void start(Context context) {
if (!localRepoServiceIsBound) { if (!localRepoServiceIsBound) {
Context app = context.getApplicationContext(); Context app = context.getApplicationContext();
Intent service = new Intent(app, LocalRepoService.class); Intent service = new Intent(app, serviceType);
localRepoServiceIsBound = app.bindService(service, serviceConnection, Context.BIND_AUTO_CREATE); localRepoServiceIsBound = app.bindService(service, serviceConnection, Context.BIND_AUTO_CREATE);
if (localRepoServiceIsBound) if (localRepoServiceIsBound)
app.startService(service); app.startService(service);
} }
} }
public static void stopLocalRepoService(Context context) { public void stop(Context context) {
Context app = context.getApplicationContext(); Context app = context.getApplicationContext();
if (localRepoServiceIsBound) { if (localRepoServiceIsBound) {
app.unbindService(serviceConnection); app.unbindService(serviceConnection);
localRepoServiceIsBound = false; localRepoServiceIsBound = false;
} }
app.stopService(new Intent(app, LocalRepoService.class)); app.stopService(new Intent(app, serviceType));
} }
/** /**
* Handles checking if the {@link LocalRepoService} is running, and only restarts it if it was running. * Handles checking if the {@link LocalRepoService} is running, and only restarts it if it was running.
*/ */
public static void restartLocalRepoServiceIfRunning() { public void restartIfRunning() {
if (localRepoServiceMessenger != null) { if (localRepoServiceMessenger != null) {
try { try {
Message msg = Message.obtain(null, LocalRepoService.RESTART, LocalRepoService.RESTART, 0); Message msg = Message.obtain(null, LocalRepoService.RESTART, LocalRepoService.RESTART, 0);
@ -347,7 +368,9 @@ public class FDroidApp extends Application {
} }
} }
public static boolean isLocalRepoServiceRunning() { public boolean isRunning() {
return localRepoServiceIsBound; return localRepoServiceIsBound;
} }
}
} }

View File

@ -5,11 +5,7 @@ import android.app.Notification;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.app.Service; import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter;
import android.os.AsyncTask;
import android.os.Handler; import android.os.Handler;
import android.os.IBinder; import android.os.IBinder;
import android.os.Looper; import android.os.Looper;
@ -20,8 +16,6 @@ import android.support.v4.content.LocalBroadcastManager;
import android.util.Log; import android.util.Log;
import org.fdroid.fdroid.FDroidApp; 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.R;
import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.net.LocalHTTPD; import org.fdroid.fdroid.net.LocalHTTPD;
@ -30,13 +24,13 @@ import org.fdroid.fdroid.views.swap.SwapActivity;
import java.io.IOException; import java.io.IOException;
import java.net.BindException; import java.net.BindException;
import java.util.HashMap;
import java.util.Random; import java.util.Random;
import javax.jmdns.JmDNS; import javax.jmdns.JmDNS;
import javax.jmdns.ServiceInfo; import javax.jmdns.ServiceInfo;
public class LocalRepoService extends Service { public abstract class LocalRepoService extends Service {
private static final String TAG = "LocalRepoService"; private static final String TAG = "LocalRepoService";
public static final String STATE = "org.fdroid.fdroid.action.LOCAL_REPO_STATE"; public static final String STATE = "org.fdroid.fdroid.action.LOCAL_REPO_STATE";
@ -50,9 +44,7 @@ public class LocalRepoService extends Service {
private final int NOTIFICATION = R.string.local_repo_running; private final int NOTIFICATION = R.string.local_repo_running;
private Handler webServerThreadHandler = null; private Handler webServerThreadHandler = null;
private LocalHTTPD localHttpd; protected LocalHTTPD localHttpd;
private JmDNS jmdns;
private ServiceInfo pairService;
public static final int START = 1111111; public static final int START = 1111111;
public static final int STOP = 12345678; public static final int STOP = 12345678;
@ -97,42 +89,6 @@ public class LocalRepoService extends Service {
} }
} }
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() { private void showNotification() {
notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
// launch LocalRepoActivity if the user selects this notification // launch LocalRepoActivity if the user selects this notification
@ -150,12 +106,10 @@ public class LocalRepoService extends Service {
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate();
showNotification(); showNotification();
startNetworkServices(); startNetworkServices();
Preferences.get().registerLocalRepoBonjourListeners(localRepoBonjourChangeListener);
LocalBroadcastManager.getInstance(this).registerReceiver(onWifiChange,
new IntentFilter(WifiStateChangeService.BROADCAST));
} }
@Override @Override
@ -167,6 +121,7 @@ public class LocalRepoService extends Service {
@Override @Override
public void onDestroy() { public void onDestroy() {
super.onDestroy();
new Thread() { new Thread() {
public void run() { public void run() {
stopNetworkServices(); stopNetworkServices();
@ -174,8 +129,6 @@ public class LocalRepoService extends Service {
}.start(); }.start();
notificationManager.cancel(NOTIFICATION); notificationManager.cancel(NOTIFICATION);
LocalBroadcastManager.getInstance(this).unregisterReceiver(onWifiChange);
Preferences.get().unregisterLocalRepoBonjourListeners(localRepoBonjourChangeListener);
} }
@Override @Override
@ -183,26 +136,40 @@ public class LocalRepoService extends Service {
return messenger.getBinder(); return messenger.getBinder();
} }
private void startNetworkServices() { /**
* Called immediately _after_ the webserver is started.
*/
protected abstract void onStartNetworkServices();
/**
* Called immediately _before_ the webserver is stopped.
*/
protected abstract void onStopNetworkServices();
/**
* Whether or not this particular version of LocalRepoService requires a HTTPS
* connection. In the local proxy instance, it will not require it, but in the
* wifi setting, it should use whatever preference the user selected.
*/
protected abstract boolean useHttps();
protected void startNetworkServices() {
Log.d(TAG, "Starting local repo network services"); Log.d(TAG, "Starting local repo network services");
startWebServer(); startWebServer();
if (Preferences.get().isLocalRepoBonjourEnabled())
registerMDNSService(); onStartNetworkServices();
Preferences.get().registerLocalRepoHttpsListeners(localRepoHttpsChangeListener);
} }
private void stopNetworkServices() { protected void stopNetworkServices() {
Log.d(TAG, "Stopping local repo network services"); onStopNetworkServices();
Preferences.get().unregisterLocalRepoHttpsListeners(localRepoHttpsChangeListener);
Log.d(TAG, "Unregistering MDNS service...");
unregisterMDNSService();
Log.d(TAG, "Stopping web server..."); Log.d(TAG, "Stopping web server...");
stopWebServer(); stopWebServer();
} }
private void startWebServer() { protected abstract String getIpAddressToBindTo();
protected void startWebServer() {
Runnable webServer = new Runnable() { Runnable webServer = new Runnable() {
// Tell Eclipse this is not a leak because of Looper use. // Tell Eclipse this is not a leak because of Looper use.
@SuppressLint("HandlerLeak") @SuppressLint("HandlerLeak")
@ -210,8 +177,10 @@ public class LocalRepoService extends Service {
public void run() { public void run() {
localHttpd = new LocalHTTPD( localHttpd = new LocalHTTPD(
LocalRepoService.this, LocalRepoService.this,
getIpAddressToBindTo(),
FDroidApp.port,
getFilesDir(), getFilesDir(),
Preferences.get().isLocalRepoHttpsEnabled()); useHttps());
Looper.prepare(); // must be run before creating a Handler Looper.prepare(); // must be run before creating a Handler
webServerThreadHandler = new Handler() { webServerThreadHandler = new Handler() {
@ -254,59 +223,5 @@ public class LocalRepoService extends Service {
LocalBroadcastManager.getInstance(LocalRepoService.this).sendBroadcast(intent); 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

@ -2,9 +2,9 @@ package org.fdroid.fdroid.net;
import android.content.Context; import android.content.Context;
import android.util.Log; import android.util.Log;
import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.Preferences;
import javax.net.ssl.SSLHandshakeException;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
@ -16,8 +16,6 @@ import java.net.Proxy;
import java.net.SocketAddress; import java.net.SocketAddress;
import java.net.URL; import java.net.URL;
import javax.net.ssl.SSLHandshakeException;
public class HttpDownloader extends Downloader { public class HttpDownloader extends Downloader {
private static final String TAG = "HttpDownloader"; private static final String TAG = "HttpDownloader";
@ -26,6 +24,7 @@ public class HttpDownloader extends Downloader {
protected HttpURLConnection connection; protected HttpURLConnection connection;
private int statusCode = -1; private int statusCode = -1;
private boolean onlyStream = false;
// The context is required for opening the file to write to. // The context is required for opening the file to write to.
HttpDownloader(String source, File destFile) HttpDownloader(String source, File destFile)
@ -39,11 +38,22 @@ public class HttpDownloader extends Downloader {
* you are done*. * you are done*.
* @see org.fdroid.fdroid.net.Downloader#getFile() * @see org.fdroid.fdroid.net.Downloader#getFile()
*/ */
HttpDownloader(String source, Context ctx) throws IOException { public HttpDownloader(String source, Context ctx) throws IOException {
super(ctx); super(ctx);
sourceUrl = new URL(source); sourceUrl = new URL(source);
} }
/**
* Calling this makes this downloader not download a file. Instead, it will
* only stream the file through the {@link HttpDownloader#getInputStream()}
* @return
*/
public HttpDownloader streamDontDownload()
{
onlyStream = true;
return this;
}
@Override @Override
public InputStream getInputStream() throws IOException { public InputStream getInputStream() throws IOException {
setupConnection(); setupConnection();

View File

@ -36,8 +36,8 @@ public class LocalHTTPD extends NanoHTTPD {
private final File webRoot; private final File webRoot;
private final boolean logRequests; private final boolean logRequests;
public LocalHTTPD(Context context, File webRoot, boolean useHttps) { public LocalHTTPD(Context context, String hostname, int port, File webRoot, boolean useHttps) {
super(FDroidApp.ipAddressString, FDroidApp.port); super(hostname, port);
this.logRequests = false; this.logRequests = false;
this.webRoot = webRoot; this.webRoot = webRoot;
this.context = context.getApplicationContext(); this.context = context.getApplicationContext();

View File

@ -151,7 +151,7 @@ public class WifiStateChangeService extends Service {
Intent intent = new Intent(BROADCAST); Intent intent = new Intent(BROADCAST);
LocalBroadcastManager.getInstance(WifiStateChangeService.this).sendBroadcast(intent); LocalBroadcastManager.getInstance(WifiStateChangeService.this).sendBroadcast(intent);
WifiStateChangeService.this.stopSelf(); WifiStateChangeService.this.stopSelf();
FDroidApp.restartLocalRepoServiceIfRunning(); FDroidApp.localRepoWifi.restartIfRunning();
} }
} }

View File

@ -70,7 +70,7 @@ public class LocalRepoActivity extends ActionBarActivity {
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
resetNetworkInfo(); resetNetworkInfo();
setRepoSwitchChecked(FDroidApp.isLocalRepoServiceRunning()); setRepoSwitchChecked(FDroidApp.localRepoWifi.isRunning());
LocalBroadcastManager.getInstance(this).registerReceiver(onWifiChange, LocalBroadcastManager.getInstance(this).registerReceiver(onWifiChange,
new IntentFilter(WifiStateChangeService.BROADCAST)); new IntentFilter(WifiStateChangeService.BROADCAST));
@ -83,7 +83,7 @@ public class LocalRepoActivity extends ActionBarActivity {
}).execute(); }).execute();
// start repo by default // start repo by default
FDroidApp.startLocalRepoService(LocalRepoActivity.this); FDroidApp.localRepoWifi.start(LocalRepoActivity.this);
// reset the timer if viewing this Activity again // reset the timer if viewing this Activity again
if (stopTimer != null) if (stopTimer != null)
stopTimer.cancel(); stopTimer.cancel();
@ -93,7 +93,7 @@ public class LocalRepoActivity extends ActionBarActivity {
@Override @Override
public void run() { public void run() {
FDroidApp.stopLocalRepoService(LocalRepoActivity.this); FDroidApp.localRepoWifi.stop(LocalRepoActivity.this);
} }
}, 900000); // 15 minutes }, 900000); // 15 minutes
} }
@ -210,9 +210,9 @@ public class LocalRepoActivity extends ActionBarActivity {
public void onClick(View v) { public void onClick(View v) {
setRepoSwitchChecked(repoSwitch.isChecked()); setRepoSwitchChecked(repoSwitch.isChecked());
if (repoSwitch.isChecked()) { if (repoSwitch.isChecked()) {
FDroidApp.startLocalRepoService(LocalRepoActivity.this); FDroidApp.localRepoWifi.start(LocalRepoActivity.this);
} else { } else {
FDroidApp.stopLocalRepoService(LocalRepoActivity.this); FDroidApp.localRepoWifi.stop(LocalRepoActivity.this);
stopTimer.cancel(); // disable automatic stop stopTimer.cancel(); // disable automatic stop
} }
} }

View File

@ -33,7 +33,7 @@ public class QrWizardWifiNetworkActivity extends ActionBarActivity {
wifiManager = (WifiManager) getSystemService(WIFI_SERVICE); wifiManager = (WifiManager) getSystemService(WIFI_SERVICE);
wifiManager.setWifiEnabled(true); wifiManager.setWifiEnabled(true);
FDroidApp.startLocalRepoService(this); FDroidApp.localRepoWifi.start(this);
setContentView(R.layout.qr_wizard_activity); setContentView(R.layout.qr_wizard_activity);
TextView instructions = (TextView) findViewById(R.id.qrWizardInstructions); TextView instructions = (TextView) findViewById(R.id.qrWizardInstructions);

View File

@ -21,9 +21,18 @@ public abstract class ThemeableListFragment extends ListFragment {
return 0; return 0;
} }
protected View getHeaderView(LayoutInflater inflater, ViewGroup container) { protected View getHeaderView() {
return headerView;
}
private View headerView = null;
private View getHeaderView(LayoutInflater inflater, ViewGroup container) {
if (getHeaderLayout() > 0) { if (getHeaderLayout() > 0) {
return inflater.inflate(getHeaderLayout(), null, false); if (headerView == null) {
headerView = inflater.inflate(getHeaderLayout(), null, false);
}
return headerView;
} else { } else {
return null; return null;
} }

View File

@ -37,7 +37,8 @@ public class SwapActivity extends ActionBarActivity implements SwapProcessManage
private static final String STATE_WIFI_QR = "wifiQr"; private static final String STATE_WIFI_QR = "wifiQr";
private static final String STATE_BLUETOOTH_DEVICE_LIST = "bluetoothDeviceList"; private static final String STATE_BLUETOOTH_DEVICE_LIST = "bluetoothDeviceList";
private static final int REQUEST_ENABLE_BLUETOOTH = 1; private static final int REQUEST_BLUETOOTH_ENABLE = 1;
private static final int REQUEST_BLUETOOTH_DISCOVERABLE = 2;
private static final String TAG = "org.fdroid.fdroid.views.swap.SwapActivity"; private static final String TAG = "org.fdroid.fdroid.views.swap.SwapActivity";
@ -109,7 +110,7 @@ public class SwapActivity extends ActionBarActivity implements SwapProcessManage
showFragment(new StartSwapFragment(), STATE_START_SWAP); showFragment(new StartSwapFragment(), STATE_START_SWAP);
if (FDroidApp.isLocalRepoServiceRunning()) { if (FDroidApp.localRepoWifi.isRunning()) {
showSelectApps(); showSelectApps();
showJoinWifi(); showJoinWifi();
attemptToShowNfc(); attemptToShowNfc();
@ -190,8 +191,8 @@ public class SwapActivity extends ActionBarActivity implements SwapProcessManage
} }
private void ensureLocalRepoRunning() { private void ensureLocalRepoRunning() {
if (!FDroidApp.isLocalRepoServiceRunning()) { if (!FDroidApp.localRepoWifi.isRunning()) {
FDroidApp.startLocalRepoService(this); FDroidApp.localRepoWifi.start(this);
initLocalRepoTimer(900000); // 15 mins initLocalRepoTimer(900000); // 15 mins
} }
} }
@ -207,7 +208,7 @@ public class SwapActivity extends ActionBarActivity implements SwapProcessManage
shutdownLocalRepoTimer.schedule(new TimerTask() { shutdownLocalRepoTimer.schedule(new TimerTask() {
@Override @Override
public void run() { public void run() {
FDroidApp.stopLocalRepoService(SwapActivity.this); FDroidApp.localRepoWifi.stop(SwapActivity.this);
} }
}, timeoutMilliseconds); }, timeoutMilliseconds);
@ -215,11 +216,11 @@ public class SwapActivity extends ActionBarActivity implements SwapProcessManage
@Override @Override
public void stopSwapping() { public void stopSwapping() {
if (FDroidApp.isLocalRepoServiceRunning()) { if (FDroidApp.localRepoWifi.isRunning()) {
if (shutdownLocalRepoTimer != null) { if (shutdownLocalRepoTimer != null) {
shutdownLocalRepoTimer.cancel(); shutdownLocalRepoTimer.cancel();
} }
FDroidApp.stopLocalRepoService(SwapActivity.this); FDroidApp.localRepoWifi.stop(SwapActivity.this);
} }
finish(); finish();
} }
@ -242,11 +243,11 @@ public class SwapActivity extends ActionBarActivity implements SwapProcessManage
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
if (adapter.isEnabled()) { if (adapter.isEnabled()) {
Log.d(TAG, "Bluetooth enabled, will pair with device."); Log.d(TAG, "Bluetooth enabled, will pair with device.");
startBluetoothServer(); ensureBluetoothDiscoverable();
} else { } else {
Log.d(TAG, "Bluetooth disabled, asking user to enable it."); Log.d(TAG, "Bluetooth disabled, asking user to enable it.");
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BLUETOOTH); startActivityForResult(enableBtIntent, REQUEST_BLUETOOTH_ENABLE);
} }
} }
@ -254,22 +255,51 @@ public class SwapActivity extends ActionBarActivity implements SwapProcessManage
public void onActivityResult(int requestCode, int resultCode, Intent data) { public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data); super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_ENABLE_BLUETOOTH) { if (requestCode == REQUEST_BLUETOOTH_ENABLE) {
if (resultCode == RESULT_OK) { if (resultCode == RESULT_OK) {
Log.d(TAG, "User enabled Bluetooth, will pair with device."); Log.d(TAG, "User enabled Bluetooth, will make sure we are discoverable.");
startBluetoothServer(); ensureBluetoothDiscoverable();
} else { } else {
// Didn't enable bluetooth // Didn't enable bluetooth
Log.d(TAG, "User chose not to enable Bluetooth, so doing nothing (i.e. sticking with wifi)."); Log.d(TAG, "User chose not to enable Bluetooth, so doing nothing (i.e. sticking with wifi).");
} }
} else if (requestCode == REQUEST_BLUETOOTH_DISCOVERABLE) {
if (resultCode != RESULT_CANCELED) {
Log.d(TAG, "User made Bluetooth discoverable, will proceed to start bluetooth server.");
startBluetoothServer();
} else {
Log.d(TAG, "User chose not to make Bluetooth discoverable, so doing nothing (i.e. sticking with wifi).");
}
}
}
private void ensureBluetoothDiscoverable() {
Log.d(TAG, "Ensuring Bluetooth is in discoverable mode.");
if (BluetoothAdapter.getDefaultAdapter().getScanMode() != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
// TODO: Listen for BluetoothAdapter.ACTION_SCAN_MODE_CHANGED and respond if discovery
// is cancelled prematurely.
Log.d(TAG, "Not currently in discoverable mode, so prompting user to enable.");
Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
intent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
startActivityForResult(intent, REQUEST_BLUETOOTH_DISCOVERABLE);
} else {
Log.d(TAG, "Bluetooth is already discoverable, so lets start the Bluetooth server.");
startBluetoothServer();
} }
} }
private void startBluetoothServer() { private void startBluetoothServer() {
Log.d(TAG, "Starting bluetooth server."); Log.d(TAG, "Starting bluetooth server.");
new BluetoothServer().start(); if (!FDroidApp.localRepoProxy.isRunning()) {
FDroidApp.localRepoProxy.start(this);
}
new BluetoothServer(this).start();
showBluetoothDeviceList(); showBluetoothDeviceList();
} }

View File

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2006 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!--
This file was modified for F-Droid to include an additional text item beyond
simple_list_item_3. Thought we may as well make it as much as possible the
same as the original, and so should essentially build on the original one
from the Android SDK.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?android:attr/listPreferredItemHeight"
android:orientation="vertical"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
>
<TextView android:id="@android:id/text1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dip"
android:textAppearance="?android:attr/textAppearanceListItem"
/>
<TextView android:id="@android:id/text2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall"
/>
<TextView android:id="@+id/text3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall"
/>
</LinearLayout>

View File

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2006 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!--
This file was modified for F-Droid to include an additional text item beyond
simple_list_item_3. Thought we may as well make it as much as possible the
same as the original, and so should essentially build on the original one
from the Android SDK.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?android:attr/listPreferredItemHeight"
android:orientation="vertical"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
>
<TextView android:id="@android:id/text1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dip"
android:textAppearance="?android:attr/textAppearanceLarge"
/>
<TextView android:id="@android:id/text2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall"
/>
<TextView android:id="@+id/text3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall"
/>
</LinearLayout>

View File

@ -9,19 +9,43 @@
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<TextView <TextView
android:layout_width="394dp" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:id="@+id/device_ip_address" android:id="@+id/device_name_prefix"
tools:text="Your device name:\nPete's Nexus 4" android:text="Your device is"
style="@style/SwapTheme.BluetoothDeviceList.Heading"/> tools:text="Your device is"
android:textAlignment="center"
style="@style/SwapTheme.BluetoothDeviceList.Heading" android:paddingTop="10dp"
android:paddingBottom="10dp" android:textSize="24sp"/>
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
tools:text="Select from devices below" android:id="@+id/device_name"
style="@style/SwapTheme.Wizard.Text"/> android:text="Pete's Nexus 4"
tools:text="Pete's Nexus 4"
android:textAlignment="center"
style="@style/SwapTheme.BluetoothDeviceList.Heading" android:paddingBottom="10dp"/>
<ContentLoadingProgressBar <TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/device_address"
android:text="(01:02:03:ab:cd:ef)"
tools:text="(01:02:03:ab:cd:ef)"
android:textAlignment="center"
style="@style/SwapTheme.BluetoothDeviceList.Heading" android:paddingBottom="20dp" android:textSize="24sp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Select from devices below, or tap here to scan for more devices..."
tools:text="Select from devices below, or tap here to scan for more devices..."
android:textAlignment="center"
style="@style/SwapTheme.BluetoothDeviceList.Text" android:paddingLeft="20dp" android:paddingRight="20dp"
android:paddingBottom="10dp"/>
<android.support.v4.widget.ContentLoadingProgressBar
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:id="@+id/loading_indicator"/> android:id="@+id/loading_indicator"/>

View File

@ -0,0 +1,29 @@
package org.fdroid.fdroid.localrepo;
/**
* Starts the local repo service but bound to 127.0.0.1.
* Also, it does not care about whether wifi is connected or not,
* and thus doesn't care about Bonjour.
*/
public class LocalRepoProxyService extends LocalRepoService {
@Override
protected void onStartNetworkServices() {
// Do nothing
}
@Override
protected void onStopNetworkServices() {
// Do nothing
}
@Override
protected boolean useHttps() {
return false;
}
@Override
protected String getIpAddressToBindTo() {
return "127.0.0.1";
}
}

View File

@ -0,0 +1,160 @@
package org.fdroid.fdroid.localrepo;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.AsyncTask;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.net.WifiStateChangeService;
import javax.jmdns.JmDNS;
import javax.jmdns.ServiceInfo;
import java.io.IOException;
import java.util.HashMap;
public class LocalRepoWifiService extends LocalRepoService {
private static final String TAG = "org.fdroid.fdroid.localrepo.LocalRepoWifiService";
private JmDNS jmdns;
private ServiceInfo pairService;
private BroadcastReceiver onWifiChange = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent i) {
stopNetworkServices();
startNetworkServices();
}
};
@Override
public void onCreate() {
super.onCreate();
Preferences.get().registerLocalRepoBonjourListeners(localRepoBonjourChangeListener);
LocalBroadcastManager.getInstance(this).registerReceiver(onWifiChange,
new IntentFilter(WifiStateChangeService.BROADCAST));
}
@Override
public void onDestroy() {
super.onDestroy();
Preferences.get().unregisterLocalRepoBonjourListeners(localRepoBonjourChangeListener);
LocalBroadcastManager.getInstance(this).unregisterReceiver(onWifiChange);
}
@Override
protected void onStartNetworkServices() {
if (Preferences.get().isLocalRepoBonjourEnabled())
registerMDNSService();
Preferences.get().registerLocalRepoHttpsListeners(localRepoHttpsChangeListener);
}
@Override
protected void onStopNetworkServices() {
Log.d(TAG, "Stopping local repo network services");
Preferences.get().unregisterLocalRepoHttpsListeners(localRepoHttpsChangeListener);
Log.d(TAG, "Unregistering MDNS service...");
unregisterMDNSService();
}
@Override
protected boolean useHttps() {
return Preferences.get().isLocalRepoHttpsEnabled();
}
@Override
protected String getIpAddressToBindTo() {
return FDroidApp.ipAddressString;
}
private Preferences.ChangeListener localRepoBonjourChangeListener = new Preferences.ChangeListener() {
@Override
public void onPreferenceChange() {
if (localHttpd.isAlive())
if (Preferences.get().isLocalRepoBonjourEnabled())
registerMDNSService();
else
unregisterMDNSService();
}
};
private Preferences.ChangeListener localRepoHttpsChangeListener = new Preferences.ChangeListener() {
@Override
public void onPreferenceChange() {
Log.i("localRepoHttpsChangeListener", "onPreferenceChange");
if (localHttpd.isAlive()) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
stopNetworkServices();
startNetworkServices();
return null;
}
}.execute();
}
}
};
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<String, String>();
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) {
e.printStackTrace();
}
}
}).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();
try {
jmdns.close();
} catch (IOException e) {
e.printStackTrace();
}
jmdns = null;
}
}
}

View File

@ -1,7 +1,7 @@
package org.fdroid.fdroid.net.bluetooth; package org.fdroid.fdroid.net.bluetooth;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import java.io.IOException; import java.io.IOException;
@ -9,29 +9,17 @@ public class BluetoothClient {
private static final String TAG = "org.fdroid.fdroid.net.bluetooth.BluetoothClient"; private static final String TAG = "org.fdroid.fdroid.net.bluetooth.BluetoothClient";
private final BluetoothAdapter adapter;
private BluetoothDevice device; private BluetoothDevice device;
public BluetoothClient() { public BluetoothClient(BluetoothDevice device) {
this.adapter = BluetoothAdapter.getDefaultAdapter(); this.device = device;
}
public void pairWithDevice() throws IOException {
if (adapter.getBondedDevices().size() == 0) {
throw new IOException("No paired Bluetooth devices.");
}
// TODO: Don't just take a random bluetooth device :)
device = adapter.getBondedDevices().iterator().next();
device.createRfcommSocketToServiceRecord(BluetoothConstants.fdroidUuid());
} }
public BluetoothConnection openConnection() throws IOException { public BluetoothConnection openConnection() throws IOException {
return null; BluetoothSocket socket = device.createRfcommSocketToServiceRecord(BluetoothConstants.fdroidUuid());
// return new BluetoothConnection(); BluetoothConnection connection = new BluetoothConnection(socket);
connection.open();
return connection;
} }
} }

View File

@ -3,9 +3,12 @@ package org.fdroid.fdroid.net.bluetooth;
import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothServerSocket; import android.bluetooth.BluetoothServerSocket;
import android.bluetooth.BluetoothSocket; import android.bluetooth.BluetoothSocket;
import android.content.Context;
import android.util.Log; import android.util.Log;
import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.net.HttpDownloader;
import org.fdroid.fdroid.net.bluetooth.httpish.Request; import org.fdroid.fdroid.net.bluetooth.httpish.Request;
import org.fdroid.fdroid.net.bluetooth.httpish.Response;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
@ -20,9 +23,14 @@ public class BluetoothServer extends Thread {
private static final String TAG = "org.fdroid.fdroid.net.bluetooth.BluetoothServer"; private static final String TAG = "org.fdroid.fdroid.net.bluetooth.BluetoothServer";
private BluetoothServerSocket serverSocket; private BluetoothServerSocket serverSocket;
private List<Connection> clients = new ArrayList<Connection>(); private List<Connection> clients = new ArrayList<Connection>();
private final Context context;
public BluetoothServer(Context context) {
this.context = context.getApplicationContext();
}
public void close() { public void close() {
for (Connection connection : clients) { for (Connection connection : clients) {
@ -50,7 +58,7 @@ public class BluetoothServer extends Thread {
try { try {
BluetoothSocket clientSocket = serverSocket.accept(); BluetoothSocket clientSocket = serverSocket.accept();
if (clientSocket != null && !isInterrupted()) { if (clientSocket != null && !isInterrupted()) {
Connection client = new Connection(clientSocket); Connection client = new Connection(context, clientSocket);
client.start(); client.start();
clients.add(client); clients.add(client);
} else { } else {
@ -67,9 +75,12 @@ public class BluetoothServer extends Thread {
{ {
private static final String TAG = "org.fdroid.fdroid.net.bluetooth.BluetoothServer.Connection"; private static final String TAG = "org.fdroid.fdroid.net.bluetooth.BluetoothServer.Connection";
private BluetoothSocket socket;
public Connection(BluetoothSocket socket) { private final Context context;
private final BluetoothSocket socket;
public Connection(Context context, BluetoothSocket socket) {
this.context = context.getApplicationContext();
this.socket = socket; this.socket = socket;
} }
@ -81,6 +92,7 @@ public class BluetoothServer extends Thread {
BluetoothConnection connection; BluetoothConnection connection;
try { try {
connection = new BluetoothConnection(socket); connection = new BluetoothConnection(socket);
connection.open();
} catch (IOException e) { } catch (IOException e) {
Log.e(TAG, "Error listening for incoming connections over bluetooth - " + e.getMessage()); Log.e(TAG, "Error listening for incoming connections over bluetooth - " + e.getMessage());
return; return;
@ -91,7 +103,7 @@ public class BluetoothServer extends Thread {
try { try {
Log.d(TAG, "Listening for new Bluetooth request from client."); Log.d(TAG, "Listening for new Bluetooth request from client.");
Request incomingRequest = Request.listenForRequest(connection); Request incomingRequest = Request.listenForRequest(connection);
handleRequest(incomingRequest); handleRequest(incomingRequest).send(connection);
} catch (IOException e) { } catch (IOException e) {
Log.e(TAG, "Error receiving incoming connection over bluetooth - " + e.getMessage()); Log.e(TAG, "Error receiving incoming connection over bluetooth - " + e.getMessage());
} }
@ -103,10 +115,31 @@ public class BluetoothServer extends Thread {
} }
private void handleRequest(Request request) { private Response handleRequest(Request request) throws IOException {
Log.d(TAG, "Received Bluetooth request from client, will process it now."); Log.d(TAG, "Received Bluetooth request from client, will process it now.");
try {
HttpDownloader downloader = new HttpDownloader("http://127.0.0.1/" + request.getPath(), context);
Response.ResponseBuilder builder;
if (request.getMethod().equals(Request.Methods.HEAD)) {
builder = new Response.ResponseBuilder();
} else {
builder = new Response.ResponseBuilder(downloader.getInputStream());
}
// TODO: At this stage, will need to download the file to get this info.
// However, should be able to make totalDownloadSize and getCacheTag work without downloading.
return builder
.setStatusCode(200)
.build();
} catch (IOException e) {
throw new IOException("Error getting file " + request.getPath() + " from local repo proxy - " + e.getMessage(), e);
}
} }
} }
} }

View File

@ -1,8 +1,13 @@
package org.fdroid.fdroid.net.bluetooth.httpish; package org.fdroid.fdroid.net.bluetooth.httpish;
import android.util.Log;
import org.fdroid.fdroid.net.bluetooth.BluetoothConnection; import org.fdroid.fdroid.net.bluetooth.BluetoothConnection;
import java.io.*; import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.HashMap; import java.util.HashMap;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
@ -10,6 +15,8 @@ import java.util.Map;
public class Request { public class Request {
private static final String TAG = "org.fdroid.fdroid.net.bluetooth.httpish.Request";
public static interface Methods { public static interface Methods {
public static final String HEAD = "HEAD"; public static final String HEAD = "HEAD";
public static final String GET = "GET"; public static final String GET = "GET";
@ -27,6 +34,9 @@ public class Request {
this.method = method; this.method = method;
this.path = path; this.path = path;
this.connection = connection; this.connection = connection;
output = new BufferedWriter(new OutputStreamWriter(connection.getOutputStream()));
input = new BufferedReader(new InputStreamReader(connection.getInputStream()));
} }
public static Request createHEAD(String path, BluetoothConnection connection) public static Request createHEAD(String path, BluetoothConnection connection)
@ -40,8 +50,7 @@ public class Request {
public Response send() throws IOException { public Response send() throws IOException {
output = new BufferedWriter(new OutputStreamWriter(connection.getOutputStream())); Log.d(TAG, "Sending request to server (" + path + ")");
input = new BufferedReader(new InputStreamReader(connection.getInputStream()));
output.write(method); output.write(method);
output.write(' '); output.write(' ');
@ -49,12 +58,23 @@ public class Request {
output.write("\n\n"); output.write("\n\n");
output.flush();
Log.d(TAG, "Finished sending request, now attempting to read response status code...");
int responseCode = readResponseCode(); int responseCode = readResponseCode();
Log.d(TAG, "Read response code " + responseCode + " from server, now reading headers...");
Map<String, String> headers = readHeaders(); Map<String, String> headers = readHeaders();
Log.d(TAG, "Read " + headers.size() + " headers");
if (method.equals(Methods.HEAD)) { if (method.equals(Methods.HEAD)) {
Log.d(TAG, "Request was a " + Methods.HEAD + " request, not including anything other than headers and status...");
return new Response(responseCode, headers); return new Response(responseCode, headers);
} else { } else {
Log.d(TAG, "Request was a " + Methods.GET + " request, so including content stream in response...");
return new Response(responseCode, headers, connection.getInputStream()); return new Response(responseCode, headers, connection.getInputStream());
} }
@ -109,9 +129,9 @@ public class Request {
// TODO: Error handling // TODO: Error handling
int firstSpace = line.indexOf(' '); int firstSpace = line.indexOf(' ');
int secondSpace = line.indexOf(' ', firstSpace); int secondSpace = line.indexOf(' ', firstSpace + 1);
String status = line.substring(firstSpace, secondSpace); String status = line.substring(firstSpace + 1, secondSpace);
return Integer.parseInt(status); return Integer.parseInt(status);
} }
@ -135,6 +155,12 @@ public class Request {
return headers; return headers;
} }
public String getPath() {
return path;
}
public String getMethod() {
return method;
}
} }

View File

@ -1,13 +1,22 @@
package org.fdroid.fdroid.net.bluetooth.httpish; package org.fdroid.fdroid.net.bluetooth.httpish;
import android.util.Log;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.net.bluetooth.BluetoothConnection;
import org.fdroid.fdroid.net.bluetooth.FileDetails; import org.fdroid.fdroid.net.bluetooth.FileDetails;
import org.fdroid.fdroid.net.bluetooth.httpish.headers.Header; import org.fdroid.fdroid.net.bluetooth.httpish.headers.Header;
import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
public class Response { public class Response {
private static final String TAG = "org.fdroid.fdroid.net.bluetooth.httpish.Response";
private int statusCode; private int statusCode;
private Map<String, String> headers; private Map<String, String> headers;
private final InputStream contentStream; private final InputStream contentStream;
@ -44,13 +53,80 @@ public class Response {
return details; return details;
} }
/**
* After parsing a response,
*/
public InputStream toContentStream() throws UnsupportedOperationException { public InputStream toContentStream() throws UnsupportedOperationException {
if (contentStream == null) { if (contentStream == null) {
throw new UnsupportedOperationException("This kind of response doesn't have a content stream. Did you perform a HEAD request instead of a GET request?"); throw new UnsupportedOperationException("This kind of response doesn't have a content stream. Did you perform a HEAD request instead of a GET request?");
} }
return contentStream; return contentStream;
} }
public void send(BluetoothConnection connection) throws IOException {
Log.d(TAG, "Sending Bluetooth HTTP-ish response...");
Writer output = new OutputStreamWriter(connection.getOutputStream());
output.write("HTTP(ish)/0.1 200 OK\n");
for (Map.Entry<String, String> entry : headers.entrySet()) {
output.write(entry.getKey());
output.write(": ");
output.write(entry.getValue());
output.write("\n");
}
output.write("\n");
output.flush();
if (contentStream != null) {
Utils.copy(contentStream, connection.getOutputStream());
}
output.flush();
}
public static class ResponseBuilder {
private InputStream contentStream;
private int statusCode = 200;
private int fileSize = -1;
private String etag = null;
public ResponseBuilder() {}
public ResponseBuilder(InputStream contentStream) {
this.contentStream = contentStream;
}
public ResponseBuilder setStatusCode(int statusCode) {
this.statusCode = statusCode;
return this;
}
public ResponseBuilder setFileSize(int fileSize) {
this.fileSize = fileSize;
return this;
}
public ResponseBuilder setETag(String etag) {
this.etag = etag;
return this;
}
public Response build() {
Map<String, String> headers = new HashMap<>(3);
if (fileSize > 0) {
headers.put("Content-Length", Integer.toString(fileSize));
}
if (etag != null) {
headers.put( "ETag", etag);
}
return new Response(statusCode, headers, contentStream);
}
}
} }

View File

@ -1,26 +1,168 @@
package org.fdroid.fdroid.views.swap; package org.fdroid.fdroid.views.swap;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.database.Cursor; import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.widget.ContentLoadingProgressBar;
import android.util.Log;
import android.view.ContextThemeWrapper; import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import android.widget.ListView; import android.widget.ListView;
import android.widget.TextView; import android.widget.TextView;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.R; import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.InstalledAppProvider; import org.fdroid.fdroid.net.bluetooth.BluetoothClient;
import org.fdroid.fdroid.net.bluetooth.BluetoothConnection;
import org.fdroid.fdroid.net.bluetooth.httpish.Request;
import org.fdroid.fdroid.net.bluetooth.httpish.Response;
import org.fdroid.fdroid.views.fragments.ThemeableListFragment; import org.fdroid.fdroid.views.fragments.ThemeableListFragment;
import java.io.IOException;
import java.util.List; import java.util.List;
public class BluetoothDeviceListFragment extends ThemeableListFragment { public class BluetoothDeviceListFragment extends ThemeableListFragment {
private static final String TAG = "org.fdroid.fdroid.views.swap.BluetoothDeviceListFragment";
private Adapter adapter = null; private Adapter adapter = null;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(false);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
setEmptyText("No bluetooth devices found. Is the other device \"discoverable\"?");
adapter = new Adapter(
new ContextThemeWrapper(getActivity(), R.style.SwapTheme_BluetoothDeviceList_ListItem),
R.layout.select_local_apps_list_item
);
populateDeviceList();
setListAdapter(adapter);
setListShown(false); // start out with a progress indicator
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = super.onCreateView(inflater, container, savedInstanceState);
View headerView = getHeaderView();
if (headerView == null) {
Log.e(TAG, "Could not find header view, although we expected one to exist.");
} else {
headerView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
initiateBluetoothScan();
return true;
}
});
}
return view;
}
private void initiateBluetoothScan()
{
BluetoothAdapter bluetooth = BluetoothAdapter.getDefaultAdapter();
BroadcastReceiver deviceFoundReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (BluetoothDevice.ACTION_FOUND.equals(intent.getAction())) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
boolean exists = false;
for (int i = 0; i < adapter.getCount(); i ++) {
if (adapter.getItem(i).getAddress().equals(device.getAddress())) {
exists = true;
break;
}
}
if (!exists) {
adapter.add(device);
}
}
}
};
((ContentLoadingProgressBar)getView().findViewById(R.id.loading_indicator)).show();
getActivity().registerReceiver(deviceFoundReceiver, new IntentFilter(BluetoothDevice.ACTION_FOUND));
if (!bluetooth.startDiscovery()) {
// TODO: Discovery did not start for some reason :(
Log.e(TAG, "Could not start bluetooth discovery, but am not sure why :(");
}
}
private void populateDeviceList()
{
for (BluetoothDevice device : BluetoothAdapter.getDefaultAdapter().getBondedDevices()) {
adapter.add(device);
}
}
@Override
public void onListItemClick(ListView l, View v, int position, long id) {
// "position" includes the header view, so ignore that.
if (position == 0) {
return;
}
BluetoothDevice device = adapter.getItem(position - 1);
// TODO: I think that I can connect regardless of the bond state.
// It sounds like when I attempt to connect to a non-bonded peer, then
// Android initiates the pairing dialog on our behalf.
BluetoothClient client = new BluetoothClient(device);
try {
Log.d(TAG, "Testing bluetooth connection (opening connection first).");
BluetoothConnection connection = client.openConnection();
Log.d(TAG, "Creating HEAD request for resource at \"/\"...");
Request head = Request.createHEAD("/", connection);
Log.d(TAG, "Sending request...");
Response response = head.send();
Log.d(TAG, "Response from bluetooth: " + response.getStatusCode());
} catch (IOException e) {
}
/*if (device.getBondState() == BluetoothDevice.BOND_NONE) {
// attempt to bond
} else if (device.getBondState() == BluetoothDevice.BOND_BONDING) {
// wait for bonding to finish
} else if (device.getBondState() == BluetoothDevice.BOND_BONDED) {
// connect
BluetoothClient client = new BluetoothClient(device);
}*/
}
@Override
protected int getThemeStyle() {
return R.style.SwapTheme_BluetoothDeviceList;
}
@Override
protected int getHeaderLayout() {
return R.layout.swap_bluetooth_header;
}
private class Adapter extends ArrayAdapter<BluetoothDevice> { private class Adapter extends ArrayAdapter<BluetoothDevice> {
public Adapter(Context context, int resource) { public Adapter(Context context, int resource) {
@ -51,61 +193,37 @@ public class BluetoothDeviceListFragment extends ThemeableListFragment {
public View getView(int position, View convertView, ViewGroup parent) { public View getView(int position, View convertView, ViewGroup parent) {
View view; View view;
if (convertView == null) { if (convertView == null) {
view = getActivity().getLayoutInflater().inflate(android.R.layout.simple_list_item_2, parent); view = getActivity().getLayoutInflater().inflate(R.layout.simple_list_item_3, null);
} else { } else {
view = convertView; view = convertView;
} }
BluetoothDevice device = getItem(position); BluetoothDevice device = getItem(position);
TextView nameView = (TextView)view.findViewById(android.R.id.text1); TextView nameView = (TextView)view.findViewById(android.R.id.text1);
TextView descriptionView = (TextView)view.findViewById(android.R.id.text2); TextView addressView = (TextView)view.findViewById(android.R.id.text2);
TextView descriptionView = (TextView)view.findViewById(R.id.text3);
nameView.setText(device.getName()); nameView.setText(device.getName());
descriptionView.setText(device.getAddress()); addressView.setText(device.getAddress());
descriptionView.setText(bondStateToLabel(device.getBondState()));
return view; return view;
} }
}
@Override private String bondStateToLabel(int deviceBondState)
public void onCreate(Bundle savedInstanceState) { {
super.onCreate(savedInstanceState); if (deviceBondState == BluetoothDevice.BOND_BONDED) {
setHasOptionsMenu(false); // TODO: Is the term "Bonded device" common parlance among phone users?
} // It sounds a bit technical to me, maybe something more lay like "Previously connected".
// Although it is technically not as accurate, it would make sense to more people...
@Override return "Bonded";
public void onActivityCreated(Bundle savedInstanceState) { } else if (deviceBondState == BluetoothDevice.BOND_BONDING) {
super.onActivityCreated(savedInstanceState); return "Currently bonding...";
setEmptyText("No bluetooth devices found. Is the other device \"discoverable\"?");
Adapter adapter = new Adapter(
new ContextThemeWrapper(getActivity(), R.style.SwapTheme_BluetoothDeviceList_ListItem),
R.layout.select_local_apps_list_item
);
setListAdapter(adapter);
setListShown(false); // start out with a progress indicator
}
@Override
public void onListItemClick(ListView l, View v, int position, long id) {
Cursor c = (Cursor) l.getAdapter().getItem(position);
String packageName = c.getString(c.getColumnIndex(InstalledAppProvider.DataColumns.APP_ID));
if (FDroidApp.selectedApps.contains(packageName)) {
FDroidApp.selectedApps.remove(packageName);
} else { } else {
FDroidApp.selectedApps.add(packageName); // TODO: Might be a little bit harsh, makes it sound more malicious than it should.
return "Unknown device";
}
} }
} }
@Override
protected int getThemeStyle() {
return R.style.SwapTheme_StartSwap;
}
@Override
protected int getHeaderLayout() {
return R.layout.swap_bluetooth_header;
}
} }