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=".net.WifiStateChangeService" />
<service android:name=".localrepo.LocalRepoService" />
<service android:name=".localrepo.LocalRepoWifiService" />
<service android:name=".localrepo.LocalRepoProxyService" />
</application>
</manifest>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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">
<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"/>

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

View File

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

View File

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

View File

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

View File

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