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:
parent
fba02e32b5
commit
7dff9a9499
@ -456,7 +456,8 @@
|
||||
|
||||
<service android:name=".UpdateService" />
|
||||
<service android:name=".net.WifiStateChangeService" />
|
||||
<service android:name=".localrepo.LocalRepoService" />
|
||||
<service android:name=".localrepo.LocalRepoWifiService" />
|
||||
<service android:name=".localrepo.LocalRepoProxyService" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
@ -43,15 +43,20 @@
|
||||
<item name="android:background">@color/white</item>
|
||||
</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 name="SwapTheme.BluetoothDeviceList.ListItem" parent="AppThemeLightWithDarkActionBar">
|
||||
</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:textColor">#222</item>
|
||||
</style>
|
||||
|
||||
<style name="SwapTheme.AppList" parent="AppThemeLightWithDarkActionBar">
|
||||
|
@ -52,7 +52,9 @@ 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.LocalRepoProxyService;
|
||||
import org.fdroid.fdroid.localrepo.LocalRepoService;
|
||||
import org.fdroid.fdroid.localrepo.LocalRepoWifiService;
|
||||
import org.fdroid.fdroid.net.IconDownloader;
|
||||
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.
|
||||
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";
|
||||
|
||||
@ -91,6 +91,9 @@ public class FDroidApp extends Application {
|
||||
|
||||
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() {
|
||||
curTheme = Theme.valueOf(PreferenceManager
|
||||
.getDefaultSharedPreferences(getBaseContext())
|
||||
@ -302,52 +305,72 @@ public class FDroidApp extends Application {
|
||||
}
|
||||
}
|
||||
|
||||
private static final ServiceConnection serviceConnection = new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName className, IBinder service) {
|
||||
localRepoServiceMessenger = new Messenger(service);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName className) {
|
||||
localRepoServiceMessenger = null;
|
||||
}
|
||||
};
|
||||
|
||||
public static void startLocalRepoService(Context context) {
|
||||
if (!localRepoServiceIsBound) {
|
||||
Context app = context.getApplicationContext();
|
||||
Intent service = new Intent(app, LocalRepoService.class);
|
||||
localRepoServiceIsBound = app.bindService(service, serviceConnection, Context.BIND_AUTO_CREATE);
|
||||
if (localRepoServiceIsBound)
|
||||
app.startService(service);
|
||||
}
|
||||
}
|
||||
|
||||
public static void stopLocalRepoService(Context context) {
|
||||
Context app = context.getApplicationContext();
|
||||
if (localRepoServiceIsBound) {
|
||||
app.unbindService(serviceConnection);
|
||||
localRepoServiceIsBound = false;
|
||||
}
|
||||
app.stopService(new Intent(app, LocalRepoService.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles checking if the {@link LocalRepoService} is running, and only restarts it if it was running.
|
||||
* 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 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 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
|
||||
public void onServiceConnected(ComponentName className, IBinder service) {
|
||||
localRepoServiceMessenger = new Messenger(service);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName className) {
|
||||
localRepoServiceMessenger = null;
|
||||
}
|
||||
};
|
||||
|
||||
public void start(Context context) {
|
||||
if (!localRepoServiceIsBound) {
|
||||
Context app = context.getApplicationContext();
|
||||
Intent service = new Intent(app, serviceType);
|
||||
localRepoServiceIsBound = app.bindService(service, serviceConnection, Context.BIND_AUTO_CREATE);
|
||||
if (localRepoServiceIsBound)
|
||||
app.startService(service);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isLocalRepoServiceRunning() {
|
||||
return localRepoServiceIsBound;
|
||||
public void stop(Context context) {
|
||||
Context app = context.getApplicationContext();
|
||||
if (localRepoServiceIsBound) {
|
||||
app.unbindService(serviceConnection);
|
||||
localRepoServiceIsBound = false;
|
||||
}
|
||||
app.stopService(new Intent(app, serviceType));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles checking if the {@link LocalRepoService} is running, and only restarts it if it was running.
|
||||
*/
|
||||
public void restartIfRunning() {
|
||||
if (localRepoServiceMessenger != null) {
|
||||
try {
|
||||
Message msg = Message.obtain(null, LocalRepoService.RESTART, LocalRepoService.RESTART, 0);
|
||||
localRepoServiceMessenger.send(msg);
|
||||
} catch (RemoteException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isRunning() {
|
||||
return localRepoServiceIsBound;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -5,11 +5,7 @@ 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;
|
||||
@ -20,8 +16,6 @@ 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;
|
||||
@ -30,13 +24,13 @@ import org.fdroid.fdroid.views.swap.SwapActivity;
|
||||
|
||||
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 {
|
||||
public abstract class LocalRepoService extends Service {
|
||||
|
||||
private static final String TAG = "LocalRepoService";
|
||||
|
||||
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 Handler webServerThreadHandler = null;
|
||||
private LocalHTTPD localHttpd;
|
||||
private JmDNS jmdns;
|
||||
private ServiceInfo pairService;
|
||||
protected LocalHTTPD localHttpd;
|
||||
|
||||
public static final int START = 1111111;
|
||||
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() {
|
||||
notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
||||
// launch LocalRepoActivity if the user selects this notification
|
||||
@ -150,12 +106,10 @@ public class LocalRepoService extends Service {
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
showNotification();
|
||||
startNetworkServices();
|
||||
Preferences.get().registerLocalRepoBonjourListeners(localRepoBonjourChangeListener);
|
||||
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(onWifiChange,
|
||||
new IntentFilter(WifiStateChangeService.BROADCAST));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -167,6 +121,7 @@ public class LocalRepoService extends Service {
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
new Thread() {
|
||||
public void run() {
|
||||
stopNetworkServices();
|
||||
@ -174,8 +129,6 @@ public class LocalRepoService extends Service {
|
||||
}.start();
|
||||
|
||||
notificationManager.cancel(NOTIFICATION);
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(onWifiChange);
|
||||
Preferences.get().unregisterLocalRepoBonjourListeners(localRepoBonjourChangeListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -183,26 +136,40 @@ public class LocalRepoService extends Service {
|
||||
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");
|
||||
startWebServer();
|
||||
if (Preferences.get().isLocalRepoBonjourEnabled())
|
||||
registerMDNSService();
|
||||
Preferences.get().registerLocalRepoHttpsListeners(localRepoHttpsChangeListener);
|
||||
|
||||
onStartNetworkServices();
|
||||
}
|
||||
|
||||
private void stopNetworkServices() {
|
||||
Log.d(TAG, "Stopping local repo network services");
|
||||
Preferences.get().unregisterLocalRepoHttpsListeners(localRepoHttpsChangeListener);
|
||||
|
||||
Log.d(TAG, "Unregistering MDNS service...");
|
||||
unregisterMDNSService();
|
||||
protected void stopNetworkServices() {
|
||||
onStopNetworkServices();
|
||||
|
||||
Log.d(TAG, "Stopping web server...");
|
||||
stopWebServer();
|
||||
}
|
||||
|
||||
private void startWebServer() {
|
||||
protected abstract String getIpAddressToBindTo();
|
||||
|
||||
protected void startWebServer() {
|
||||
Runnable webServer = new Runnable() {
|
||||
// Tell Eclipse this is not a leak because of Looper use.
|
||||
@SuppressLint("HandlerLeak")
|
||||
@ -210,8 +177,10 @@ public class LocalRepoService extends Service {
|
||||
public void run() {
|
||||
localHttpd = new LocalHTTPD(
|
||||
LocalRepoService.this,
|
||||
getIpAddressToBindTo(),
|
||||
FDroidApp.port,
|
||||
getFilesDir(),
|
||||
Preferences.get().isLocalRepoHttpsEnabled());
|
||||
useHttps());
|
||||
|
||||
Looper.prepare(); // must be run before creating a Handler
|
||||
webServerThreadHandler = new Handler() {
|
||||
@ -254,59 +223,5 @@ public class LocalRepoService extends Service {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,9 +2,9 @@ package org.fdroid.fdroid.net;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import org.fdroid.fdroid.Preferences;
|
||||
|
||||
import javax.net.ssl.SSLHandshakeException;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
@ -16,8 +16,6 @@ import java.net.Proxy;
|
||||
import java.net.SocketAddress;
|
||||
import java.net.URL;
|
||||
|
||||
import javax.net.ssl.SSLHandshakeException;
|
||||
|
||||
public class HttpDownloader extends Downloader {
|
||||
private static final String TAG = "HttpDownloader";
|
||||
|
||||
@ -26,6 +24,7 @@ public class HttpDownloader extends Downloader {
|
||||
|
||||
protected HttpURLConnection connection;
|
||||
private int statusCode = -1;
|
||||
private boolean onlyStream = false;
|
||||
|
||||
// The context is required for opening the file to write to.
|
||||
HttpDownloader(String source, File destFile)
|
||||
@ -39,11 +38,22 @@ public class HttpDownloader extends Downloader {
|
||||
* you are done*.
|
||||
* @see org.fdroid.fdroid.net.Downloader#getFile()
|
||||
*/
|
||||
HttpDownloader(String source, Context ctx) throws IOException {
|
||||
public HttpDownloader(String source, Context ctx) throws IOException {
|
||||
super(ctx);
|
||||
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
|
||||
public InputStream getInputStream() throws IOException {
|
||||
setupConnection();
|
||||
|
@ -36,8 +36,8 @@ public class LocalHTTPD extends NanoHTTPD {
|
||||
private final File webRoot;
|
||||
private final boolean logRequests;
|
||||
|
||||
public LocalHTTPD(Context context, File webRoot, boolean useHttps) {
|
||||
super(FDroidApp.ipAddressString, FDroidApp.port);
|
||||
public LocalHTTPD(Context context, String hostname, int port, File webRoot, boolean useHttps) {
|
||||
super(hostname, port);
|
||||
this.logRequests = false;
|
||||
this.webRoot = webRoot;
|
||||
this.context = context.getApplicationContext();
|
||||
|
@ -151,7 +151,7 @@ public class WifiStateChangeService extends Service {
|
||||
Intent intent = new Intent(BROADCAST);
|
||||
LocalBroadcastManager.getInstance(WifiStateChangeService.this).sendBroadcast(intent);
|
||||
WifiStateChangeService.this.stopSelf();
|
||||
FDroidApp.restartLocalRepoServiceIfRunning();
|
||||
FDroidApp.localRepoWifi.restartIfRunning();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -70,7 +70,7 @@ public class LocalRepoActivity extends ActionBarActivity {
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
resetNetworkInfo();
|
||||
setRepoSwitchChecked(FDroidApp.isLocalRepoServiceRunning());
|
||||
setRepoSwitchChecked(FDroidApp.localRepoWifi.isRunning());
|
||||
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(onWifiChange,
|
||||
new IntentFilter(WifiStateChangeService.BROADCAST));
|
||||
@ -83,7 +83,7 @@ public class LocalRepoActivity extends ActionBarActivity {
|
||||
}).execute();
|
||||
|
||||
// start repo by default
|
||||
FDroidApp.startLocalRepoService(LocalRepoActivity.this);
|
||||
FDroidApp.localRepoWifi.start(LocalRepoActivity.this);
|
||||
// reset the timer if viewing this Activity again
|
||||
if (stopTimer != null)
|
||||
stopTimer.cancel();
|
||||
@ -93,7 +93,7 @@ public class LocalRepoActivity extends ActionBarActivity {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
FDroidApp.stopLocalRepoService(LocalRepoActivity.this);
|
||||
FDroidApp.localRepoWifi.stop(LocalRepoActivity.this);
|
||||
}
|
||||
}, 900000); // 15 minutes
|
||||
}
|
||||
@ -210,9 +210,9 @@ public class LocalRepoActivity extends ActionBarActivity {
|
||||
public void onClick(View v) {
|
||||
setRepoSwitchChecked(repoSwitch.isChecked());
|
||||
if (repoSwitch.isChecked()) {
|
||||
FDroidApp.startLocalRepoService(LocalRepoActivity.this);
|
||||
FDroidApp.localRepoWifi.start(LocalRepoActivity.this);
|
||||
} else {
|
||||
FDroidApp.stopLocalRepoService(LocalRepoActivity.this);
|
||||
FDroidApp.localRepoWifi.stop(LocalRepoActivity.this);
|
||||
stopTimer.cancel(); // disable automatic stop
|
||||
}
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ public class QrWizardWifiNetworkActivity extends ActionBarActivity {
|
||||
|
||||
wifiManager = (WifiManager) getSystemService(WIFI_SERVICE);
|
||||
wifiManager.setWifiEnabled(true);
|
||||
FDroidApp.startLocalRepoService(this);
|
||||
FDroidApp.localRepoWifi.start(this);
|
||||
|
||||
setContentView(R.layout.qr_wizard_activity);
|
||||
TextView instructions = (TextView) findViewById(R.id.qrWizardInstructions);
|
||||
|
@ -21,9 +21,18 @@ public abstract class ThemeableListFragment extends ListFragment {
|
||||
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) {
|
||||
return inflater.inflate(getHeaderLayout(), null, false);
|
||||
if (headerView == null) {
|
||||
headerView = inflater.inflate(getHeaderLayout(), null, false);
|
||||
}
|
||||
return headerView;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
@ -37,7 +37,8 @@ public class SwapActivity extends ActionBarActivity implements SwapProcessManage
|
||||
private static final String STATE_WIFI_QR = "wifiQr";
|
||||
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";
|
||||
|
||||
@ -109,7 +110,7 @@ public class SwapActivity extends ActionBarActivity implements SwapProcessManage
|
||||
|
||||
showFragment(new StartSwapFragment(), STATE_START_SWAP);
|
||||
|
||||
if (FDroidApp.isLocalRepoServiceRunning()) {
|
||||
if (FDroidApp.localRepoWifi.isRunning()) {
|
||||
showSelectApps();
|
||||
showJoinWifi();
|
||||
attemptToShowNfc();
|
||||
@ -190,8 +191,8 @@ public class SwapActivity extends ActionBarActivity implements SwapProcessManage
|
||||
}
|
||||
|
||||
private void ensureLocalRepoRunning() {
|
||||
if (!FDroidApp.isLocalRepoServiceRunning()) {
|
||||
FDroidApp.startLocalRepoService(this);
|
||||
if (!FDroidApp.localRepoWifi.isRunning()) {
|
||||
FDroidApp.localRepoWifi.start(this);
|
||||
initLocalRepoTimer(900000); // 15 mins
|
||||
}
|
||||
}
|
||||
@ -207,7 +208,7 @@ public class SwapActivity extends ActionBarActivity implements SwapProcessManage
|
||||
shutdownLocalRepoTimer.schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
FDroidApp.stopLocalRepoService(SwapActivity.this);
|
||||
FDroidApp.localRepoWifi.stop(SwapActivity.this);
|
||||
}
|
||||
}, timeoutMilliseconds);
|
||||
|
||||
@ -215,11 +216,11 @@ public class SwapActivity extends ActionBarActivity implements SwapProcessManage
|
||||
|
||||
@Override
|
||||
public void stopSwapping() {
|
||||
if (FDroidApp.isLocalRepoServiceRunning()) {
|
||||
if (FDroidApp.localRepoWifi.isRunning()) {
|
||||
if (shutdownLocalRepoTimer != null) {
|
||||
shutdownLocalRepoTimer.cancel();
|
||||
}
|
||||
FDroidApp.stopLocalRepoService(SwapActivity.this);
|
||||
FDroidApp.localRepoWifi.stop(SwapActivity.this);
|
||||
}
|
||||
finish();
|
||||
}
|
||||
@ -242,11 +243,11 @@ public class SwapActivity extends ActionBarActivity implements SwapProcessManage
|
||||
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
|
||||
if (adapter.isEnabled()) {
|
||||
Log.d(TAG, "Bluetooth enabled, will pair with device.");
|
||||
startBluetoothServer();
|
||||
ensureBluetoothDiscoverable();
|
||||
} else {
|
||||
Log.d(TAG, "Bluetooth disabled, asking user to enable it.");
|
||||
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) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
if (requestCode == REQUEST_ENABLE_BLUETOOTH) {
|
||||
if (requestCode == REQUEST_BLUETOOTH_ENABLE) {
|
||||
|
||||
if (resultCode == RESULT_OK) {
|
||||
Log.d(TAG, "User enabled Bluetooth, will pair with device.");
|
||||
startBluetoothServer();
|
||||
Log.d(TAG, "User enabled Bluetooth, will make sure we are discoverable.");
|
||||
ensureBluetoothDiscoverable();
|
||||
} else {
|
||||
// Didn't enable bluetooth
|
||||
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() {
|
||||
Log.d(TAG, "Starting bluetooth server.");
|
||||
new BluetoothServer().start();
|
||||
if (!FDroidApp.localRepoProxy.isRunning()) {
|
||||
FDroidApp.localRepoProxy.start(this);
|
||||
}
|
||||
new BluetoothServer(this).start();
|
||||
showBluetoothDeviceList();
|
||||
}
|
||||
|
||||
|
52
res/layout-v14/simple_list_item_3.xml
Normal file
52
res/layout-v14/simple_list_item_3.xml
Normal 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>
|
52
res/layout/simple_list_item_3.xml
Normal file
52
res/layout/simple_list_item_3.xml
Normal 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>
|
@ -9,19 +9,43 @@
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:layout_width="394dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/device_ip_address"
|
||||
tools:text="Your device name:\nPete's Nexus 4"
|
||||
style="@style/SwapTheme.BluetoothDeviceList.Heading"/>
|
||||
android:id="@+id/device_name_prefix"
|
||||
android:text="Your device is"
|
||||
tools:text="Your device is"
|
||||
android:textAlignment="center"
|
||||
style="@style/SwapTheme.BluetoothDeviceList.Heading" android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp" android:textSize="24sp"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="Select from devices below"
|
||||
style="@style/SwapTheme.Wizard.Text"/>
|
||||
android:id="@+id/device_name"
|
||||
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_height="wrap_content"
|
||||
android:id="@+id/loading_indicator"/>
|
||||
|
29
src/org/fdroid/fdroid/localrepo/LocalRepoProxyService.java
Normal file
29
src/org/fdroid/fdroid/localrepo/LocalRepoProxyService.java
Normal 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";
|
||||
}
|
||||
}
|
160
src/org/fdroid/fdroid/localrepo/LocalRepoWifiService.java
Normal file
160
src/org/fdroid/fdroid/localrepo/LocalRepoWifiService.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
package org.fdroid.fdroid.net.bluetooth;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.bluetooth.BluetoothSocket;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@ -9,29 +9,17 @@ public class BluetoothClient {
|
||||
|
||||
private static final String TAG = "org.fdroid.fdroid.net.bluetooth.BluetoothClient";
|
||||
|
||||
private final BluetoothAdapter adapter;
|
||||
private BluetoothDevice device;
|
||||
|
||||
public BluetoothClient() {
|
||||
this.adapter = BluetoothAdapter.getDefaultAdapter();
|
||||
}
|
||||
|
||||
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 BluetoothClient(BluetoothDevice device) {
|
||||
this.device = device;
|
||||
}
|
||||
|
||||
public BluetoothConnection openConnection() throws IOException {
|
||||
return null;
|
||||
// return new BluetoothConnection();
|
||||
BluetoothSocket socket = device.createRfcommSocketToServiceRecord(BluetoothConstants.fdroidUuid());
|
||||
BluetoothConnection connection = new BluetoothConnection(socket);
|
||||
connection.open();
|
||||
return connection;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -3,9 +3,12 @@ package org.fdroid.fdroid.net.bluetooth;
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothServerSocket;
|
||||
import android.bluetooth.BluetoothSocket;
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
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.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
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 BluetoothServerSocket serverSocket;
|
||||
|
||||
private List<Connection> clients = new ArrayList<Connection>();
|
||||
|
||||
private final Context context;
|
||||
|
||||
public BluetoothServer(Context context) {
|
||||
this.context = context.getApplicationContext();
|
||||
}
|
||||
|
||||
public void close() {
|
||||
|
||||
for (Connection connection : clients) {
|
||||
@ -50,7 +58,7 @@ public class BluetoothServer extends Thread {
|
||||
try {
|
||||
BluetoothSocket clientSocket = serverSocket.accept();
|
||||
if (clientSocket != null && !isInterrupted()) {
|
||||
Connection client = new Connection(clientSocket);
|
||||
Connection client = new Connection(context, clientSocket);
|
||||
client.start();
|
||||
clients.add(client);
|
||||
} else {
|
||||
@ -67,9 +75,12 @@ public class BluetoothServer extends Thread {
|
||||
{
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@ -81,6 +92,7 @@ public class BluetoothServer extends Thread {
|
||||
BluetoothConnection connection;
|
||||
try {
|
||||
connection = new BluetoothConnection(socket);
|
||||
connection.open();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Error listening for incoming connections over bluetooth - " + e.getMessage());
|
||||
return;
|
||||
@ -91,7 +103,7 @@ public class BluetoothServer extends Thread {
|
||||
try {
|
||||
Log.d(TAG, "Listening for new Bluetooth request from client.");
|
||||
Request incomingRequest = Request.listenForRequest(connection);
|
||||
handleRequest(incomingRequest);
|
||||
handleRequest(incomingRequest).send(connection);
|
||||
} catch (IOException e) {
|
||||
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.");
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,13 @@
|
||||
package org.fdroid.fdroid.net.bluetooth.httpish;
|
||||
|
||||
import android.util.Log;
|
||||
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.Locale;
|
||||
import java.util.Map;
|
||||
@ -10,6 +15,8 @@ import java.util.Map;
|
||||
public class Request {
|
||||
|
||||
|
||||
private static final String TAG = "org.fdroid.fdroid.net.bluetooth.httpish.Request";
|
||||
|
||||
public static interface Methods {
|
||||
public static final String HEAD = "HEAD";
|
||||
public static final String GET = "GET";
|
||||
@ -27,6 +34,9 @@ public class Request {
|
||||
this.method = method;
|
||||
this.path = path;
|
||||
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)
|
||||
@ -40,8 +50,7 @@ public class Request {
|
||||
|
||||
public Response send() throws IOException {
|
||||
|
||||
output = new BufferedWriter(new OutputStreamWriter(connection.getOutputStream()));
|
||||
input = new BufferedReader(new InputStreamReader(connection.getInputStream()));
|
||||
Log.d(TAG, "Sending request to server (" + path + ")");
|
||||
|
||||
output.write(method);
|
||||
output.write(' ');
|
||||
@ -49,12 +58,23 @@ public class Request {
|
||||
|
||||
output.write("\n\n");
|
||||
|
||||
output.flush();
|
||||
|
||||
Log.d(TAG, "Finished sending request, now attempting to read response status code...");
|
||||
|
||||
int responseCode = readResponseCode();
|
||||
|
||||
Log.d(TAG, "Read response code " + responseCode + " from server, now reading headers...");
|
||||
|
||||
Map<String, String> headers = readHeaders();
|
||||
|
||||
Log.d(TAG, "Read " + headers.size() + " headers");
|
||||
|
||||
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);
|
||||
} else {
|
||||
Log.d(TAG, "Request was a " + Methods.GET + " request, so including content stream in response...");
|
||||
return new Response(responseCode, headers, connection.getInputStream());
|
||||
}
|
||||
|
||||
@ -109,9 +129,9 @@ public class Request {
|
||||
|
||||
// TODO: Error handling
|
||||
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);
|
||||
}
|
||||
|
||||
@ -135,6 +155,12 @@ public class Request {
|
||||
return headers;
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
public String getMethod() {
|
||||
return method;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,13 +1,22 @@
|
||||
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.httpish.headers.Header;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.Writer;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class Response {
|
||||
|
||||
private static final String TAG = "org.fdroid.fdroid.net.bluetooth.httpish.Response";
|
||||
|
||||
private int statusCode;
|
||||
private Map<String, String> headers;
|
||||
private final InputStream contentStream;
|
||||
@ -44,13 +53,80 @@ public class Response {
|
||||
return details;
|
||||
}
|
||||
|
||||
/**
|
||||
* After parsing a response,
|
||||
*/
|
||||
public InputStream toContentStream() throws UnsupportedOperationException {
|
||||
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?");
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1,26 +1,168 @@
|
||||
package org.fdroid.fdroid.views.swap;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.widget.ContentLoadingProgressBar;
|
||||
import android.util.Log;
|
||||
import android.view.ContextThemeWrapper;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
import org.fdroid.fdroid.FDroidApp;
|
||||
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 java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
public class BluetoothDeviceListFragment extends ThemeableListFragment {
|
||||
|
||||
private static final String TAG = "org.fdroid.fdroid.views.swap.BluetoothDeviceListFragment";
|
||||
|
||||
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> {
|
||||
|
||||
public Adapter(Context context, int resource) {
|
||||
@ -51,61 +193,37 @@ public class BluetoothDeviceListFragment extends ThemeableListFragment {
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
View view;
|
||||
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 {
|
||||
view = convertView;
|
||||
}
|
||||
|
||||
BluetoothDevice device = getItem(position);
|
||||
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());
|
||||
descriptionView.setText(device.getAddress());
|
||||
addressView.setText(device.getAddress());
|
||||
descriptionView.setText(bondStateToLabel(device.getBondState()));
|
||||
|
||||
return view;
|
||||
}
|
||||
}
|
||||
|
||||
@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 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 {
|
||||
FDroidApp.selectedApps.add(packageName);
|
||||
private String bondStateToLabel(int deviceBondState)
|
||||
{
|
||||
if (deviceBondState == BluetoothDevice.BOND_BONDED) {
|
||||
// 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...
|
||||
return "Bonded";
|
||||
} else if (deviceBondState == BluetoothDevice.BOND_BONDING) {
|
||||
return "Currently bonding...";
|
||||
} else {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user