Merge branch 'swap/bluetooth' into swap-demolition-material-mockups
This commit is contained in:
commit
b0b285bc1f
@ -44,6 +44,7 @@
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="18" />
|
||||
|
52
F-Droid/res/layout-v14/simple_list_item_3.xml
Normal file
52
F-Droid/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
F-Droid/res/layout/simple_list_item_3.xml
Normal file
52
F-Droid/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>
|
10
F-Droid/res/layout/swap_bluetooth_devices.xml
Normal file
10
F-Droid/res/layout/swap_bluetooth_devices.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<org.fdroid.fdroid.views.swap.BluetoothDeviceListView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/swap_blue">
|
||||
|
||||
</org.fdroid.fdroid.views.swap.BluetoothDeviceListView>
|
61
F-Droid/res/layout/swap_bluetooth_header.xml
Normal file
61
F-Droid/res/layout/swap_bluetooth_header.xml
Normal file
@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<LinearLayout
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/device_name_prefix"
|
||||
android:text="@string/swap_bluetooth_your_device"
|
||||
android:gravity="center_horizontal"
|
||||
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"
|
||||
android:id="@+id/device_name"
|
||||
tools:text="Phone v2.0"
|
||||
android:gravity="center_horizontal"
|
||||
style="@style/SwapTheme.BluetoothDeviceList.Heading"
|
||||
android:paddingBottom="10dp"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/device_address"
|
||||
tools:text="(01:02:03:ab:cd:ef)"
|
||||
android:gravity="center_horizontal"
|
||||
style="@style/SwapTheme.BluetoothDeviceList.Heading"
|
||||
android:paddingBottom="20dp"
|
||||
android:textSize="24sp"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/swap_bluetooth_select_or_scan"
|
||||
android:gravity="center_horizontal"
|
||||
style="@style/SwapTheme.BluetoothDeviceList.Text"
|
||||
android:paddingLeft="20dp"
|
||||
android:paddingRight="20dp"
|
||||
android:paddingBottom="10dp"/>
|
||||
|
||||
<android.support.v4.widget.ContentLoadingProgressBar
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/loading_indicator"
|
||||
android:paddingLeft="5dp"
|
||||
android:paddingRight="5dp"
|
||||
android:visibility="gone"
|
||||
android:indeterminate="true"
|
||||
style="?android:attr/android:progressBarStyleHorizontal"/>
|
||||
|
||||
</LinearLayout>
|
@ -26,12 +26,12 @@
|
||||
android:layout_below="@+id/text_description"
|
||||
android:layout_centerHorizontal="true" />
|
||||
|
||||
<!--
|
||||
<Button style="@style/SwapTheme.Wizard.OptionButton"
|
||||
android:id="@+id/btn_bluetooth"
|
||||
android:text="@string/swap_use_bluetooth"
|
||||
android:layout_alignParentBottom="true" />
|
||||
|
||||
<!--
|
||||
<Button style="@style/SwapTheme.Wizard.OptionButton"
|
||||
android:text="@string/swap_wifi_help"
|
||||
android:layout_above="@id/btn_bluetooth"
|
||||
|
14
F-Droid/res/menu/swap_scan.xml
Normal file
14
F-Droid/res/menu/swap_scan.xml
Normal file
@ -0,0 +1,14 @@
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
|
||||
|
||||
<item
|
||||
android:id="@+id/action_scan"
|
||||
android:title="Scan for devices"
|
||||
android:titleCondensed="Scan"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/action_cancel"
|
||||
android:title="Cancel scan"
|
||||
android:titleCondensed="@string/cancel"
|
||||
android:visible="false"/>
|
||||
|
||||
</menu>
|
@ -331,6 +331,11 @@
|
||||
<string name="wifi_warning_public">May work</string>
|
||||
<string name="wifi_warning_private">Promising</string>
|
||||
<string name="wifi_warning_personal">Best bet</string>
|
||||
<string name="swap_bluetooth_your_device">Your device is</string>
|
||||
<string name="swap_bluetooth_select_or_scan">Select from devices below, or press \"Scan\" from the menu to find more devices.</string>
|
||||
<string name="swap_bluetooth_bonded_device">Bonded</string>
|
||||
<string name="swap_bluetooth_bonding_device">Currently bonding...</string>
|
||||
<string name="swap_bluetooth_unknown_device">Unknown device</string>
|
||||
<string name="swap_choose_apps">Choose Apps</string>
|
||||
<string name="swap_scan_qr">Scan QR Code</string>
|
||||
</resources>
|
||||
|
@ -43,6 +43,22 @@
|
||||
<item name="android:background">@color/white</item>
|
||||
</style>
|
||||
|
||||
<style name="SwapTheme.StartSwap.Text" parent="@style/SwapTheme.StartSwap">
|
||||
</style>
|
||||
|
||||
<style name="SwapTheme.BluetoothDeviceList" parent="@style/SwapTheme.Wizard">
|
||||
</style>
|
||||
|
||||
<style name="SwapTheme.BluetoothDeviceList.ListItem" parent="AppThemeDark">
|
||||
</style>
|
||||
|
||||
<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 -->
|
||||
</style>
|
||||
|
||||
<style name="SwapTheme.AppList" parent="AppThemeLight">
|
||||
</style>
|
||||
|
||||
|
230
F-Droid/src/org/apache/commons/io/input/BoundedInputStream.java
Normal file
230
F-Droid/src/org/apache/commons/io/input/BoundedInputStream.java
Normal file
@ -0,0 +1,230 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.
|
||||
*/
|
||||
package org.apache.commons.io.input;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* This is a stream that will only supply bytes up to a certain length - if its
|
||||
* position goes above that, it will stop.
|
||||
* <p>
|
||||
* This is useful to wrap ServletInputStreams. The ServletInputStream will block
|
||||
* if you try to read content from it that isn't there, because it doesn't know
|
||||
* whether the content hasn't arrived yet or whether the content has finished.
|
||||
* So, one of these, initialized with the Content-length sent in the
|
||||
* ServletInputStream's header, will stop it blocking, providing it's been sent
|
||||
* with a correct content length.
|
||||
*
|
||||
* @version $Id: BoundedInputStream.java 1307462 2012-03-30 15:13:11Z ggregory $
|
||||
* @since 2.0
|
||||
*/
|
||||
public class BoundedInputStream extends InputStream {
|
||||
|
||||
/** the wrapped input stream */
|
||||
private final InputStream in;
|
||||
|
||||
/** the max length to provide */
|
||||
private final long max;
|
||||
|
||||
/** the number of bytes already returned */
|
||||
private long pos = 0;
|
||||
|
||||
/** the marked position */
|
||||
private long mark = -1;
|
||||
|
||||
/** flag if close shoud be propagated */
|
||||
private boolean propagateClose = true;
|
||||
|
||||
/**
|
||||
* Creates a new <code>BoundedInputStream</code> that wraps the given input
|
||||
* stream and limits it to a certain size.
|
||||
*
|
||||
* @param in The wrapped input stream
|
||||
* @param size The maximum number of bytes to return
|
||||
*/
|
||||
public BoundedInputStream(InputStream in, long size) {
|
||||
// Some badly designed methods - eg the servlet API - overload length
|
||||
// such that "-1" means stream finished
|
||||
this.max = size;
|
||||
this.in = in;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new <code>BoundedInputStream</code> that wraps the given input
|
||||
* stream and is unlimited.
|
||||
*
|
||||
* @param in The wrapped input stream
|
||||
*/
|
||||
public BoundedInputStream(InputStream in) {
|
||||
this(in, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the delegate's <code>read()</code> method if
|
||||
* the current position is less than the limit.
|
||||
* @return the byte read or -1 if the end of stream or
|
||||
* the limit has been reached.
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
if (max >= 0 && pos >= max) {
|
||||
return -1;
|
||||
}
|
||||
int result = in.read();
|
||||
pos++;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the delegate's <code>read(byte[])</code> method.
|
||||
* @param b the buffer to read the bytes into
|
||||
* @return the number of bytes read or -1 if the end of stream or
|
||||
* the limit has been reached.
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
@Override
|
||||
public int read(byte[] b) throws IOException {
|
||||
return this.read(b, 0, b.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the delegate's <code>read(byte[], int, int)</code> method.
|
||||
* @param b the buffer to read the bytes into
|
||||
* @param off The start offset
|
||||
* @param len The number of bytes to read
|
||||
* @return the number of bytes read or -1 if the end of stream or
|
||||
* the limit has been reached.
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
if (max>=0 && pos>=max) {
|
||||
return -1;
|
||||
}
|
||||
long maxRead = max>=0 ? Math.min(len, max-pos) : len;
|
||||
int bytesRead = in.read(b, off, (int)maxRead);
|
||||
|
||||
if (bytesRead==-1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
pos+=bytesRead;
|
||||
return bytesRead;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the delegate's <code>skip(long)</code> method.
|
||||
* @param n the number of bytes to skip
|
||||
* @return the actual number of bytes skipped
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
@Override
|
||||
public long skip(long n) throws IOException {
|
||||
long toSkip = max>=0 ? Math.min(n, max-pos) : n;
|
||||
long skippedBytes = in.skip(toSkip);
|
||||
pos+=skippedBytes;
|
||||
return skippedBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public int available() throws IOException {
|
||||
if (max>=0 && pos>=max) {
|
||||
return 0;
|
||||
}
|
||||
return in.available();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the delegate's <code>toString()</code> method.
|
||||
* @return the delegate's <code>toString()</code>
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return in.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the delegate's <code>close()</code> method
|
||||
* if {@link #isPropagateClose()} is {@code true}.
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if (propagateClose) {
|
||||
in.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the delegate's <code>reset()</code> method.
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
@Override
|
||||
public synchronized void reset() throws IOException {
|
||||
in.reset();
|
||||
pos = mark;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the delegate's <code>mark(int)</code> method.
|
||||
* @param readlimit read ahead limit
|
||||
*/
|
||||
@Override
|
||||
public synchronized void mark(int readlimit) {
|
||||
in.mark(readlimit);
|
||||
mark = pos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the delegate's <code>markSupported()</code> method.
|
||||
* @return true if mark is supported, otherwise false
|
||||
*/
|
||||
@Override
|
||||
public boolean markSupported() {
|
||||
return in.markSupported();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether the {@link #close()} method
|
||||
* should propagate to the underling {@link InputStream}.
|
||||
*
|
||||
* @return {@code true} if calling {@link #close()}
|
||||
* propagates to the <code>close()</code> method of the
|
||||
* underlying stream or {@code false} if it does not.
|
||||
*/
|
||||
public boolean isPropagateClose() {
|
||||
return propagateClose;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether the {@link #close()} method
|
||||
* should propagate to the underling {@link InputStream}.
|
||||
*
|
||||
* @param propagateClose {@code true} if calling
|
||||
* {@link #close()} propagates to the <code>close()</code>
|
||||
* method of the underlying stream or
|
||||
* {@code false} if it does not.
|
||||
*/
|
||||
public void setPropagateClose(boolean propagateClose) {
|
||||
this.propagateClose = propagateClose;
|
||||
}
|
||||
}
|
@ -69,6 +69,7 @@ public class SwapManager {
|
||||
public static final int STEP_JOIN_WIFI = 3;
|
||||
public static final int STEP_SHOW_NFC = 4;
|
||||
public static final int STEP_WIFI_QR = 5;
|
||||
public static final int STEP_BLUETOOTH = 6;
|
||||
|
||||
private @SwapStep int step = STEP_INTRO;
|
||||
|
||||
@ -96,7 +97,7 @@ public class SwapManager {
|
||||
* be passed rather than in integer, however that is harder to persist on disk than an int.
|
||||
* This is the same as, e.g. {@link Context#getSystemService(String)}
|
||||
*/
|
||||
@IntDef({STEP_INTRO, STEP_SELECT_APPS, STEP_JOIN_WIFI, STEP_SHOW_NFC, STEP_WIFI_QR})
|
||||
@IntDef({STEP_INTRO, STEP_SELECT_APPS, STEP_JOIN_WIFI, STEP_SHOW_NFC, STEP_WIFI_QR, STEP_BLUETOOTH})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
public @interface SwapStep {}
|
||||
|
||||
|
@ -39,6 +39,8 @@ public class WebServerType implements SwapType {
|
||||
public void run() {
|
||||
localHttpd = new LocalHTTPD(
|
||||
context,
|
||||
FDroidApp.ipAddressString,
|
||||
FDroidApp.port,
|
||||
context.getFilesDir(),
|
||||
Preferences.get().isLocalRepoHttpsEnabled());
|
||||
|
||||
|
105
F-Droid/src/org/fdroid/fdroid/net/BluetoothDownloader.java
Normal file
105
F-Droid/src/org/fdroid/fdroid/net/BluetoothDownloader.java
Normal file
@ -0,0 +1,105 @@
|
||||
package org.fdroid.fdroid.net;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import org.apache.commons.io.input.BoundedInputStream;
|
||||
import org.fdroid.fdroid.net.bluetooth.BluetoothConnection;
|
||||
import org.fdroid.fdroid.net.bluetooth.FileDetails;
|
||||
import org.fdroid.fdroid.net.bluetooth.httpish.Request;
|
||||
import org.fdroid.fdroid.net.bluetooth.httpish.Response;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.MalformedURLException;
|
||||
|
||||
public class BluetoothDownloader extends Downloader {
|
||||
|
||||
private static final String TAG = "BluetoothDownloader";
|
||||
|
||||
private final BluetoothConnection connection;
|
||||
private FileDetails fileDetails;
|
||||
private final String sourcePath;
|
||||
|
||||
public BluetoothDownloader(BluetoothConnection connection, String sourcePath, Context ctx) throws IOException {
|
||||
super(ctx);
|
||||
this.connection = connection;
|
||||
this.sourcePath = sourcePath;
|
||||
}
|
||||
|
||||
public BluetoothDownloader(BluetoothConnection connection, String sourcePath, File destFile) throws FileNotFoundException, MalformedURLException {
|
||||
super(destFile);
|
||||
this.connection = connection;
|
||||
this.sourcePath = sourcePath;
|
||||
}
|
||||
|
||||
public BluetoothDownloader(BluetoothConnection connection, String sourcePath, OutputStream output) throws MalformedURLException {
|
||||
super(output);
|
||||
this.connection = connection;
|
||||
this.sourcePath = sourcePath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() throws IOException {
|
||||
Response response = Request.createGET(sourcePath, connection).send();
|
||||
fileDetails = response.toFileDetails();
|
||||
|
||||
// TODO: Manage the dependency which includes this class better?
|
||||
// Right now, I only needed the one class from apache commons.
|
||||
// There are countless classes online which provide this functionality,
|
||||
// including some which are available from the Android SDK - the only
|
||||
// problem is that they have a funky API which doesn't just wrap a
|
||||
// plain old InputStream (the class is ContentLengthInputStream -
|
||||
// whereas this BoundedInputStream is much more generic and useful
|
||||
// to us).
|
||||
BoundedInputStream stream = new BoundedInputStream(response.toContentStream(), fileDetails.getFileSize());
|
||||
stream.setPropagateClose(false);
|
||||
return stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* May return null if an error occurred while getting file details.
|
||||
* TODO: Should we throw an exception? Everywhere else in this blue package throws IO exceptions weelx`x`xy-neely.
|
||||
* Will probably require some thought as to how the API looks, with regards to all of the public methods
|
||||
* and their signatures.
|
||||
*/
|
||||
public FileDetails getFileDetails() {
|
||||
if (fileDetails == null) {
|
||||
Log.d(TAG, "Going to Bluetooth \"server\" to get file details.");
|
||||
try {
|
||||
fileDetails = Request.createHEAD(sourceUrl.getPath(), connection).send().toFileDetails();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Error getting file details from Bluetooth \"server\": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
return fileDetails;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasChanged() {
|
||||
return getFileDetails().getCacheTag() == null || getFileDetails().getCacheTag().equals(getCacheTag());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int totalDownloadSize() {
|
||||
return getFileDetails().getFileSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void download() throws IOException, InterruptedException {
|
||||
downloadFromStream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCached() {
|
||||
FileDetails details = getFileDetails();
|
||||
return (
|
||||
details != null &&
|
||||
details.getCacheTag() != null &&
|
||||
details.getCacheTag().equals(getCacheTag())
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -127,7 +127,8 @@ public abstract class Downloader {
|
||||
// we were interrupted before proceeding to the download.
|
||||
throwExceptionIfInterrupted();
|
||||
|
||||
copyInputToOutputStream(getInputStream());
|
||||
// TODO: Check side effects of changing this second getInputStream() to input.
|
||||
copyInputToOutputStream(input);
|
||||
} finally {
|
||||
Utils.closeQuietly(outputStream);
|
||||
Utils.closeQuietly(input);
|
||||
@ -173,12 +174,13 @@ public abstract class Downloader {
|
||||
int count = input.read(buffer);
|
||||
throwExceptionIfInterrupted();
|
||||
|
||||
bytesRead += count;
|
||||
sendProgress(bytesRead, totalBytes);
|
||||
if (count == -1) {
|
||||
Log.d(TAG, "Finished downloading from stream");
|
||||
break;
|
||||
}
|
||||
|
||||
bytesRead += count;
|
||||
sendProgress(bytesRead, totalBytes);
|
||||
outputStream.write(buffer, 0, count);
|
||||
}
|
||||
outputStream.flush();
|
||||
|
@ -1,27 +1,38 @@
|
||||
package org.fdroid.fdroid.net;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
public class DownloaderFactory {
|
||||
|
||||
public static Downloader create(String url, Context context)
|
||||
throws IOException {
|
||||
if (isOnionAddress(url)) {
|
||||
public static Downloader create(String url, Context context) throws IOException {
|
||||
Uri uri = Uri.parse(url);
|
||||
if (isBluetoothAddress(uri)) {
|
||||
return new BluetoothDownloader(null, uri.getPath(), context);
|
||||
} else if (isOnionAddress(url)) {
|
||||
return new TorHttpDownloader(url, context);
|
||||
}
|
||||
} else {
|
||||
return new HttpDownloader(url, context);
|
||||
}
|
||||
|
||||
public static Downloader create(String url, File destFile)
|
||||
throws IOException {
|
||||
if (isOnionAddress(url)) {
|
||||
return new TorHttpDownloader(url, destFile);
|
||||
}
|
||||
|
||||
public static Downloader create(String url, File destFile) throws IOException {
|
||||
Uri uri = Uri.parse(url);
|
||||
if (isBluetoothAddress(uri)) {
|
||||
return new BluetoothDownloader(null, uri.getPath(), destFile);
|
||||
} else if (isOnionAddress(url)) {
|
||||
return new TorHttpDownloader(url, destFile);
|
||||
} else {
|
||||
return new HttpDownloader(url, destFile);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isBluetoothAddress(Uri uri) {
|
||||
return "bluetooth".equalsIgnoreCase(uri.getScheme());
|
||||
}
|
||||
|
||||
private static boolean isOnionAddress(String url) {
|
||||
return url.matches("^[a-zA-Z0-9]+://[^/]+\\.onion/.*");
|
||||
|
@ -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();
|
||||
@ -134,4 +144,8 @@ public class HttpDownloader extends Downloader {
|
||||
return this.statusCode != 304;
|
||||
}
|
||||
|
||||
public int getStatusCode() {
|
||||
return statusCode;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -0,0 +1,26 @@
|
||||
package org.fdroid.fdroid.net.bluetooth;
|
||||
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.bluetooth.BluetoothSocket;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class BluetoothClient {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = "BluetoothClient";
|
||||
|
||||
private BluetoothDevice device;
|
||||
|
||||
public BluetoothClient(BluetoothDevice device) {
|
||||
this.device = device;
|
||||
}
|
||||
|
||||
public BluetoothConnection openConnection() throws IOException {
|
||||
BluetoothSocket socket = device.createRfcommSocketToServiceRecord(BluetoothConstants.fdroidUuid());
|
||||
BluetoothConnection connection = new BluetoothConnection(socket);
|
||||
connection.open();
|
||||
return connection;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
package org.fdroid.fdroid.net.bluetooth;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.bluetooth.BluetoothSocket;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class BluetoothConnection {
|
||||
|
||||
private static final String TAG = "BluetoothConnection";
|
||||
|
||||
private InputStream input = null;
|
||||
private OutputStream output = null;
|
||||
protected final BluetoothSocket socket;
|
||||
|
||||
public BluetoothConnection(BluetoothSocket socket) throws IOException {
|
||||
this.socket = socket;
|
||||
}
|
||||
|
||||
public InputStream getInputStream() {
|
||||
return input;
|
||||
}
|
||||
|
||||
public OutputStream getOutputStream() {
|
||||
return output;
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
|
||||
public void open() throws IOException {
|
||||
if (!socket.isConnected()) {
|
||||
// Server sockets will already be connected when they are passed to us,
|
||||
// client sockets require us to call connect().
|
||||
socket.connect();
|
||||
}
|
||||
|
||||
input = socket.getInputStream();
|
||||
output = socket.getOutputStream();
|
||||
Log.d(TAG, "Opened connection to Bluetooth device");
|
||||
}
|
||||
|
||||
public void closeQuietly() {
|
||||
Utils.closeQuietly(input);
|
||||
Utils.closeQuietly(output);
|
||||
Utils.closeQuietly(socket);
|
||||
}
|
||||
|
||||
public void close() throws IOException {
|
||||
if (input == null || output == null) {
|
||||
throw new RuntimeException("Cannot close() a BluetoothConnection before calling open()" );
|
||||
}
|
||||
|
||||
input.close();
|
||||
output.close();
|
||||
socket.close();
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package org.fdroid.fdroid.net.bluetooth;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* We need some shared information between the client and the server app.
|
||||
*/
|
||||
public class BluetoothConstants {
|
||||
|
||||
public static UUID fdroidUuid() {
|
||||
// TODO: Generate a UUID deterministically from, e.g. "org.fdroid.fdroid.net.Bluetooth";
|
||||
// This UUID is just from the first example at http://www.ietf.org/rfc/rfc4122.txt
|
||||
return UUID.fromString("f81d4fae-7dec-11d0-a765-00a0c91e6bf6");
|
||||
}
|
||||
|
||||
}
|
152
F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java
Normal file
152
F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java
Normal file
@ -0,0 +1,152 @@
|
||||
package org.fdroid.fdroid.net.bluetooth;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothServerSocket;
|
||||
import android.bluetooth.BluetoothSocket;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
import org.fdroid.fdroid.FDroidApp;
|
||||
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;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Act as a layer on top of LocalHTTPD server, by forwarding requests served
|
||||
* over bluetooth to that server.
|
||||
*/
|
||||
public class BluetoothServer extends Thread {
|
||||
|
||||
private static final String TAG = "BluetoothServer";
|
||||
|
||||
private BluetoothServerSocket serverSocket;
|
||||
private List<Connection> clients = new ArrayList<>();
|
||||
|
||||
private final Context context;
|
||||
|
||||
public BluetoothServer(Context context) {
|
||||
this.context = context.getApplicationContext();
|
||||
}
|
||||
|
||||
public void close() {
|
||||
|
||||
for (Connection connection : clients) {
|
||||
connection.interrupt();
|
||||
}
|
||||
|
||||
if (serverSocket != null) {
|
||||
Utils.closeQuietly(serverSocket);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
|
||||
try {
|
||||
serverSocket = adapter.listenUsingRfcommWithServiceRecord("FDroid App Swap", BluetoothConstants.fdroidUuid());
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Error starting Bluetooth server socket, will stop the server now - " + e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
BluetoothSocket clientSocket = serverSocket.accept();
|
||||
if (clientSocket != null && !isInterrupted()) {
|
||||
Connection client = new Connection(context, clientSocket);
|
||||
client.start();
|
||||
clients.add(client);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Error receiving client connection over Bluetooth server socket, will continue listening for other clients - " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class Connection extends Thread
|
||||
{
|
||||
|
||||
private final Context context;
|
||||
private final BluetoothSocket socket;
|
||||
|
||||
public Connection(Context context, BluetoothSocket socket) {
|
||||
this.context = context.getApplicationContext();
|
||||
this.socket = socket;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
Log.d(TAG, "Listening for incoming Bluetooth requests from client");
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
|
||||
try {
|
||||
Log.d(TAG, "Listening for new Bluetooth request from client.");
|
||||
Request incomingRequest = Request.listenForRequest(connection);
|
||||
handleRequest(incomingRequest).send(connection);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Error receiving incoming connection over bluetooth - " + e.getMessage());
|
||||
}
|
||||
|
||||
if (isInterrupted())
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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:" + ( FDroidApp.port + 1 ) + "/" + request.getPath(), context);
|
||||
|
||||
Response.Builder builder;
|
||||
|
||||
if (request.getMethod().equals(Request.Methods.HEAD)) {
|
||||
builder = new Response.Builder();
|
||||
} else {
|
||||
builder = new Response.Builder(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(downloader.getStatusCode())
|
||||
.setFileSize(downloader.totalDownloadSize())
|
||||
.build();
|
||||
|
||||
} catch (IOException e) {
|
||||
if (Build.VERSION.SDK_INT <= 9) {
|
||||
// Would like to use the specific IOException below with a "cause", but it is
|
||||
// only supported on SDK 9, so I guess this is the next most useful thing.
|
||||
throw e;
|
||||
} else {
|
||||
throw new IOException("Error getting file " + request.getPath() + " from local repo proxy - " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
23
F-Droid/src/org/fdroid/fdroid/net/bluetooth/FileDetails.java
Normal file
23
F-Droid/src/org/fdroid/fdroid/net/bluetooth/FileDetails.java
Normal file
@ -0,0 +1,23 @@
|
||||
package org.fdroid.fdroid.net.bluetooth;
|
||||
|
||||
public class FileDetails {
|
||||
|
||||
private String cacheTag;
|
||||
private int fileSize;
|
||||
|
||||
public String getCacheTag() {
|
||||
return cacheTag;
|
||||
}
|
||||
|
||||
public int getFileSize() {
|
||||
return fileSize;
|
||||
}
|
||||
|
||||
public void setFileSize(int fileSize) {
|
||||
this.fileSize = fileSize;
|
||||
}
|
||||
|
||||
public void setCacheTag(String cacheTag) {
|
||||
this.cacheTag = cacheTag;
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package org.fdroid.fdroid.net.bluetooth;
|
||||
|
||||
public class UnexpectedResponseException extends Exception {
|
||||
|
||||
public UnexpectedResponseException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public UnexpectedResponseException(String message, Throwable cause) {
|
||||
super("Unexpected response from Bluetooth server: '" + message + "'", cause);
|
||||
}
|
||||
}
|
170
F-Droid/src/org/fdroid/fdroid/net/bluetooth/httpish/Request.java
Normal file
170
F-Droid/src/org/fdroid/fdroid/net/bluetooth/httpish/Request.java
Normal file
@ -0,0 +1,170 @@
|
||||
package org.fdroid.fdroid.net.bluetooth.httpish;
|
||||
|
||||
import android.util.Log;
|
||||
import org.fdroid.fdroid.net.bluetooth.BluetoothConnection;
|
||||
|
||||
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;
|
||||
|
||||
public class Request {
|
||||
|
||||
|
||||
private static final String TAG = "bluetooth.Request";
|
||||
|
||||
public interface Methods {
|
||||
String HEAD = "HEAD";
|
||||
String GET = "GET";
|
||||
}
|
||||
|
||||
private String method;
|
||||
private String path;
|
||||
private Map<String, String> headers;
|
||||
|
||||
private BluetoothConnection connection;
|
||||
private BufferedWriter output;
|
||||
private BufferedReader input;
|
||||
|
||||
private Request(String method, String path, BluetoothConnection connection) {
|
||||
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)
|
||||
{
|
||||
return new Request(Methods.HEAD, path, connection);
|
||||
}
|
||||
|
||||
public static Request createGET(String path, BluetoothConnection connection) {
|
||||
return new Request(Methods.GET, path, connection);
|
||||
}
|
||||
|
||||
public String getHeaderValue(String header) {
|
||||
return headers.containsKey(header) ? headers.get(header) : null;
|
||||
}
|
||||
|
||||
public Response send() throws IOException {
|
||||
|
||||
Log.d(TAG, "Sending request to server (" + path + ")");
|
||||
|
||||
output.write(method);
|
||||
output.write(' ');
|
||||
output.write(path);
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function used by listenForRequest().
|
||||
* The reason it is here is because the listenForRequest() is a static function, which would
|
||||
* need to instantiate it's own InputReaders from the bluetooth connection. However, we already
|
||||
* have that happening in a Request, so it is in some ways simpler to delegate to a member
|
||||
* method like this.
|
||||
*/
|
||||
private boolean listen() throws IOException {
|
||||
|
||||
String requestLine = input.readLine();
|
||||
|
||||
if (requestLine == null || requestLine.trim().length() == 0)
|
||||
return false;
|
||||
|
||||
String[] parts = requestLine.split("\\s+");
|
||||
|
||||
// First part is the method (GET/HEAD), second is the path (/fdroid/repo/index.jar)
|
||||
if (parts.length < 2)
|
||||
return false;
|
||||
|
||||
method = parts[0].toUpperCase(Locale.ENGLISH);
|
||||
path = parts[1];
|
||||
headers = readHeaders();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a blocking method, which will wait until a full Request is received.
|
||||
*/
|
||||
public static Request listenForRequest(BluetoothConnection connection) throws IOException {
|
||||
Request request = new Request("", "", connection);
|
||||
return request.listen() ? request : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* First line of a HTTP response is the status line:
|
||||
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1
|
||||
* The first part is the HTTP version, followed by a space, then the status code, then
|
||||
* a space, and then the status label (which may contain spaces).
|
||||
*/
|
||||
private int readResponseCode() throws IOException {
|
||||
String line = input.readLine();
|
||||
if (line == null) {
|
||||
// TODO: What to do?
|
||||
return -1;
|
||||
}
|
||||
|
||||
// TODO: Error handling
|
||||
int firstSpace = line.indexOf(' ');
|
||||
int secondSpace = line.indexOf(' ', firstSpace + 1);
|
||||
|
||||
String status = line.substring(firstSpace + 1, secondSpace);
|
||||
return Integer.parseInt(status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subsequent lines (after the status line) represent the headers, which are case
|
||||
* insensitive and may be multi-line. We don't deal with multi-line headers in
|
||||
* our HTTP-ish implementation.
|
||||
*/
|
||||
private Map<String, String> readHeaders() throws IOException {
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
String responseLine = input.readLine();
|
||||
while (responseLine != null && responseLine.length() > 0) {
|
||||
|
||||
// TODO: Error handling
|
||||
String[] parts = responseLine.split(":");
|
||||
String header = parts[0].trim();
|
||||
String value = parts[1].trim();
|
||||
headers.put(header, value);
|
||||
responseLine = input.readLine();
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
public String getMethod() {
|
||||
return method;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,161 @@
|
||||
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.ByteArrayOutputStream;
|
||||
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 = "bluetooth.Response";
|
||||
|
||||
private int statusCode;
|
||||
private Map<String, String> headers;
|
||||
private final InputStream contentStream;
|
||||
|
||||
public Response(int statusCode, Map<String, String> headers) {
|
||||
this(statusCode, headers, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* This class expects 'contentStream' to be open, and ready for use.
|
||||
* It will not close it either. However it will block wile doing things
|
||||
* so you can call a method, wait for it to finish, and then close
|
||||
* it afterwards if you like.
|
||||
*/
|
||||
public Response(int statusCode, Map<String, String> headers, InputStream contentStream) {
|
||||
this.statusCode = statusCode;
|
||||
this.headers = headers;
|
||||
this.contentStream = contentStream;
|
||||
}
|
||||
|
||||
public int getStatusCode() {
|
||||
return statusCode;
|
||||
}
|
||||
|
||||
public int getFileSize() {
|
||||
if (headers != null) {
|
||||
for (Map.Entry<String, String> entry : headers.entrySet()) {
|
||||
if (entry.getKey().toLowerCase().equals("content-length")) {
|
||||
return Integer.parseInt( entry.getValue()); // TODO: error handling.
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts meaningful headers from the response into a more useful and safe
|
||||
* {@link org.fdroid.fdroid.net.bluetooth.FileDetails} object.
|
||||
*/
|
||||
public FileDetails toFileDetails() {
|
||||
FileDetails details = new FileDetails();
|
||||
for (Map.Entry<String, String> entry : headers.entrySet()) {
|
||||
Header.process(details, entry.getKey(), entry.getValue());
|
||||
}
|
||||
return details;
|
||||
}
|
||||
|
||||
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 String readContents() throws IOException {
|
||||
int size = getFileSize();
|
||||
if (contentStream == null || getFileSize() <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int pos = 0;
|
||||
byte[] buffer = new byte[4096];
|
||||
ByteArrayOutputStream contents = new ByteArrayOutputStream(size);
|
||||
while (pos < size) {
|
||||
int read = contentStream.read(buffer);
|
||||
pos += read;
|
||||
contents.write(buffer, 0, read);
|
||||
}
|
||||
return contents.toString();
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private InputStream contentStream;
|
||||
private int statusCode = 200;
|
||||
private int fileSize = -1;
|
||||
private String etag = null;
|
||||
|
||||
public Builder() {}
|
||||
|
||||
public Builder(InputStream contentStream) {
|
||||
this.contentStream = contentStream;
|
||||
}
|
||||
|
||||
public Builder setStatusCode(int statusCode) {
|
||||
this.statusCode = statusCode;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setFileSize(int fileSize) {
|
||||
this.fileSize = fileSize;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder 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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package org.fdroid.fdroid.net.bluetooth.httpish.headers;
|
||||
|
||||
import org.fdroid.fdroid.net.bluetooth.FileDetails;
|
||||
|
||||
public class ContentLengthHeader extends Header {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "content-length";
|
||||
}
|
||||
|
||||
public void handle(FileDetails details, String value) {
|
||||
details.setFileSize(Integer.parseInt(value));
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package org.fdroid.fdroid.net.bluetooth.httpish.headers;
|
||||
|
||||
import org.fdroid.fdroid.net.bluetooth.FileDetails;
|
||||
|
||||
public class ETagHeader extends Header {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "etag";
|
||||
}
|
||||
|
||||
public void handle(FileDetails details, String value) {
|
||||
details.setCacheTag(value);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package org.fdroid.fdroid.net.bluetooth.httpish.headers;
|
||||
|
||||
import org.fdroid.fdroid.net.bluetooth.FileDetails;
|
||||
|
||||
public abstract class Header {
|
||||
|
||||
private static Header[] VALID_HEADERS = {
|
||||
new ContentLengthHeader(),
|
||||
new ETagHeader(),
|
||||
};
|
||||
|
||||
protected abstract String getName();
|
||||
protected abstract void handle(FileDetails details, String value);
|
||||
|
||||
public static void process(FileDetails details, String header, String value) {
|
||||
header = header.toLowerCase();
|
||||
for (Header potentialHeader : VALID_HEADERS) {
|
||||
if (potentialHeader.getName().equals(header)) {
|
||||
potentialHeader.handle(details, value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -0,0 +1,346 @@
|
||||
package org.fdroid.fdroid.views.swap;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.view.MenuItemCompat;
|
||||
import android.support.v4.widget.ContentLoadingProgressBar;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.ContextThemeWrapper;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
import org.fdroid.fdroid.R;
|
||||
import org.fdroid.fdroid.localrepo.SwapManager;
|
||||
import org.fdroid.fdroid.net.BluetoothDownloader;
|
||||
import org.fdroid.fdroid.net.bluetooth.BluetoothClient;
|
||||
import org.fdroid.fdroid.net.bluetooth.BluetoothConnection;
|
||||
import org.fdroid.fdroid.views.fragments.ThemeableListFragment;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
public class BluetoothDeviceListView extends ListView implements
|
||||
SwapWorkflowActivity.InnerView,
|
||||
ListView.OnItemClickListener {
|
||||
|
||||
private static final String TAG = "BluetoothDeviceListView";
|
||||
|
||||
private Adapter adapter = null;
|
||||
|
||||
private MenuItem scanMenuItem;
|
||||
private MenuItem cancelMenuItem;
|
||||
|
||||
public BluetoothDeviceListView(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public BluetoothDeviceListView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public BluetoothDeviceListView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
public BluetoothDeviceListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean buildMenu(Menu menu, @NonNull MenuInflater menuInflater) {
|
||||
menuInflater.inflate(R.menu.swap_scan, menu);
|
||||
|
||||
final int flags = MenuItemCompat.SHOW_AS_ACTION_ALWAYS | MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT;
|
||||
|
||||
scanMenuItem = menu.findItem(R.id.action_scan);
|
||||
scanMenuItem.setVisible(true);
|
||||
MenuItemCompat.setShowAsAction(scanMenuItem, flags);
|
||||
|
||||
scanMenuItem.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem item) {
|
||||
initiateBluetoothScan();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
cancelMenuItem = menu.findItem(R.id.action_cancel);
|
||||
cancelMenuItem.setVisible(false);
|
||||
MenuItemCompat.setShowAsAction(cancelMenuItem, flags);
|
||||
|
||||
cancelMenuItem.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem item) {
|
||||
cancelBluetoothScan();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getStep() {
|
||||
return SwapManager.STEP_BLUETOOTH;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPreviousStep() {
|
||||
return SwapManager.STEP_JOIN_WIFI;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getToolbarColour() {
|
||||
return R.color.swap_blue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getToolbarTitle() {
|
||||
return getContext().getString(R.string.swap_use_bluetooth);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
|
||||
adapter = new Adapter(
|
||||
getContext(),
|
||||
R.layout.select_local_apps_list_item
|
||||
);
|
||||
|
||||
LayoutInflater inflater = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
View headerView = inflater.inflate(R.layout.swap_bluetooth_header, this, false);
|
||||
addHeaderView(headerView);
|
||||
|
||||
setAdapter(adapter);
|
||||
|
||||
final BluetoothAdapter bluetooth = BluetoothAdapter.getDefaultAdapter();
|
||||
|
||||
final TextView deviceName = (TextView)headerView.findViewById(R.id.device_name);
|
||||
deviceName.setText(bluetooth.getName());
|
||||
|
||||
final TextView address = (TextView)headerView.findViewById(R.id.device_address);
|
||||
address.setText(bluetooth.getAddress());
|
||||
|
||||
populateBondedDevices();
|
||||
|
||||
}
|
||||
|
||||
private void cancelBluetoothScan() {
|
||||
|
||||
Log.d(TAG, "Cancelling bluetooth scan.");
|
||||
|
||||
cancelMenuItem.setVisible(false);
|
||||
scanMenuItem.setVisible(true);
|
||||
|
||||
final BluetoothAdapter bluetooth = BluetoothAdapter.getDefaultAdapter();
|
||||
bluetooth.cancelDiscovery();
|
||||
|
||||
getLoadingIndicator().hide();
|
||||
|
||||
}
|
||||
|
||||
private ContentLoadingProgressBar getLoadingIndicator() {
|
||||
return ((ContentLoadingProgressBar)findViewById(R.id.loading_indicator));
|
||||
}
|
||||
|
||||
private void initiateBluetoothScan()
|
||||
{
|
||||
Log.d(TAG, "Starting bluetooth scan...");
|
||||
|
||||
cancelMenuItem.setVisible(true);
|
||||
scanMenuItem.setVisible(false);
|
||||
|
||||
final ContentLoadingProgressBar loadingBar = getLoadingIndicator();
|
||||
|
||||
loadingBar.show();
|
||||
|
||||
|
||||
final BluetoothAdapter bluetooth = BluetoothAdapter.getDefaultAdapter();
|
||||
final 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);
|
||||
Log.d(TAG, "Found bluetooth device: " + device.toString());
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
final BroadcastReceiver scanCompleteReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
Log.d(TAG, "Scan complete: " + intent.getAction());
|
||||
loadingBar.hide();
|
||||
cancelMenuItem.setVisible(false);
|
||||
scanMenuItem.setVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
getContext().registerReceiver(deviceFoundReceiver, new IntentFilter(BluetoothDevice.ACTION_FOUND));
|
||||
getContext().registerReceiver(scanCompleteReceiver, new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED));
|
||||
|
||||
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 populateBondedDevices()
|
||||
{
|
||||
for (BluetoothDevice device : BluetoothAdapter.getDefaultAdapter().getBondedDevices()) {
|
||||
adapter.add(device);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, 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();
|
||||
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream(4096);
|
||||
BluetoothDownloader downloader = new BluetoothDownloader(connection, "/", stream);
|
||||
downloader.downloadUninterrupted();
|
||||
String result = stream.toString();
|
||||
Log.d(TAG, "Download complete.");
|
||||
Log.d(TAG, result);
|
||||
|
||||
Log.d(TAG, "Downloading again...");
|
||||
downloader = new BluetoothDownloader(connection, "/fdroid/repo/index.xml", stream);
|
||||
downloader.downloadUninterrupted();
|
||||
result = stream.toString();
|
||||
Log.d(TAG, "Download complete.");
|
||||
Log.d(TAG, result);
|
||||
|
||||
/*Log.d(TAG, "Creating HEAD request for resource at \"/\"...");
|
||||
Request head = Request.createGET("/", connection);
|
||||
Log.d(TAG, "Sending request...");
|
||||
Response response = head.send();
|
||||
Log.d(TAG, "Response from bluetooth: " + response.getStatusCode());
|
||||
String contents = response.readContents();
|
||||
Log.d(TAG, contents);*/
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Error: " + e.getMessage());
|
||||
}
|
||||
|
||||
/*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);
|
||||
}*/
|
||||
}
|
||||
|
||||
private class Adapter extends ArrayAdapter<BluetoothDevice> {
|
||||
|
||||
public Adapter(Context context, int resource) {
|
||||
super(context, resource);
|
||||
}
|
||||
|
||||
public Adapter(Context context, int resource, int textViewResourceId) {
|
||||
super(context, resource, textViewResourceId);
|
||||
}
|
||||
|
||||
public Adapter(Context context, int resource, BluetoothDevice[] objects) {
|
||||
super(context, resource, objects);
|
||||
}
|
||||
|
||||
public Adapter(Context context, int resource, int textViewResourceId, BluetoothDevice[] objects) {
|
||||
super(context, resource, textViewResourceId, objects);
|
||||
}
|
||||
|
||||
public Adapter(Context context, int resource, List<BluetoothDevice> objects) {
|
||||
super(context, resource, objects);
|
||||
}
|
||||
|
||||
public Adapter(Context context, int resource, int textViewResourceId, List<BluetoothDevice> objects) {
|
||||
super(context, resource, textViewResourceId, objects);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
View view;
|
||||
if (convertView == null) {
|
||||
LayoutInflater inflater = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
view = inflater.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 addressView = (TextView)view.findViewById(android.R.id.text2);
|
||||
TextView descriptionView = (TextView)view.findViewById(R.id.text3);
|
||||
|
||||
nameView.setText(device.getName() == null ? getContext().getString(R.string.unknown) : device.getName());
|
||||
addressView.setText(device.getAddress());
|
||||
descriptionView.setText(bondStateToLabel(device.getBondState()));
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
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 getContext().getString(R.string.swap_bluetooth_bonded_device);
|
||||
} else if (deviceBondState == BluetoothDevice.BOND_BONDING) {
|
||||
return getContext().getString(R.string.swap_bluetooth_bonding_device);
|
||||
} else {
|
||||
// TODO: Might be a little bit harsh, makes it sound more malicious than it should.
|
||||
return getContext().getString(R.string.swap_bluetooth_unknown_device);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -17,6 +17,7 @@ import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
@ -72,6 +73,15 @@ public class JoinWifiView extends RelativeLayout implements SwapWorkflowActivity
|
||||
new IntentFilter(WifiStateChangeService.BROADCAST)
|
||||
);
|
||||
|
||||
Button bluetooth = (Button)findViewById(R.id.btn_bluetooth);
|
||||
bluetooth.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
getActivity().connectWithBluetooth();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
// TODO: Listen for "Connecting..." state and reflect that in the view too.
|
||||
|
@ -2,6 +2,7 @@ package org.fdroid.fdroid.views.swap;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.ProgressDialog;
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
@ -13,6 +14,7 @@ import android.support.annotation.NonNull;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
import android.support.v7.app.ActionBarActivity;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
@ -32,6 +34,7 @@ import org.fdroid.fdroid.Utils;
|
||||
import org.fdroid.fdroid.data.NewRepoConfig;
|
||||
import org.fdroid.fdroid.localrepo.LocalRepoManager;
|
||||
import org.fdroid.fdroid.localrepo.SwapManager;
|
||||
import org.fdroid.fdroid.net.bluetooth.BluetoothServer;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@ -59,7 +62,11 @@ public class SwapWorkflowActivity extends ActionBarActivity {
|
||||
String getToolbarTitle();
|
||||
}
|
||||
|
||||
private static final String TAG = "SwapWorkflowActivity";
|
||||
|
||||
private static final int CONNECT_TO_SWAP = 1;
|
||||
private static final int REQUEST_BLUETOOTH_ENABLE = 2;
|
||||
private static final int REQUEST_BLUETOOTH_DISCOVERABLE = 3;
|
||||
|
||||
private Toolbar toolbar;
|
||||
private SwapManager state;
|
||||
@ -198,6 +205,10 @@ public class SwapWorkflowActivity extends ActionBarActivity {
|
||||
inflateInnerView(R.layout.swap_join_wifi);
|
||||
}
|
||||
|
||||
private void showBluetoothDeviceList() {
|
||||
inflateInnerView(R.layout.swap_bluetooth_devices);
|
||||
}
|
||||
|
||||
public void onJoinWifiComplete() {
|
||||
ensureLocalRepoRunning();
|
||||
if (!attemptToShowNfc()) {
|
||||
@ -248,9 +259,81 @@ public class SwapWorkflowActivity extends ActionBarActivity {
|
||||
}
|
||||
} else if (requestCode == CONNECT_TO_SWAP && resultCode == Activity.RESULT_OK) {
|
||||
finish();
|
||||
} else if (requestCode == REQUEST_BLUETOOTH_ENABLE) {
|
||||
|
||||
if (resultCode == RESULT_OK) {
|
||||
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).");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The process for setting up bluetooth is as follows:
|
||||
* * Assume we have bluetooth available (otherwise the button which allowed us to start
|
||||
* the bluetooth process should not have been available). TODO: Remove button if bluetooth unavailable.
|
||||
* * Ask user to enable (if not enabled yet).
|
||||
* * Start bluetooth server socket.
|
||||
* * Enable bluetooth discoverability, so that people can connect to our server socket.
|
||||
*
|
||||
* Note that this is a little different than the usual process for bluetooth _clients_, which
|
||||
* involves pairing and connecting with other devices.
|
||||
*/
|
||||
public void connectWithBluetooth() {
|
||||
|
||||
Log.d(TAG, "Initiating Bluetooth swap instead of wifi.");
|
||||
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
|
||||
// TODO: May be null (e.g. on an emulator).
|
||||
if (adapter.isEnabled()) {
|
||||
Log.d(TAG, "Bluetooth enabled, will pair with device.");
|
||||
ensureBluetoothDiscoverable();
|
||||
} else {
|
||||
Log.d(TAG, "Bluetooth disabled, asking user to enable it.");
|
||||
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
|
||||
startActivityForResult(enableBtIntent, REQUEST_BLUETOOTH_ENABLE);
|
||||
}
|
||||
}
|
||||
|
||||
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.");
|
||||
if (!state.isEnabled()) {
|
||||
state.enableSwapping();
|
||||
}
|
||||
new BluetoothServer(this).start();
|
||||
showBluetoothDeviceList();
|
||||
}
|
||||
|
||||
|
||||
class UpdateAsyncTask extends AsyncTask<Void, String, Void> {
|
||||
|
||||
@SuppressWarnings("UnusedDeclaration")
|
||||
|
Loading…
x
Reference in New Issue
Block a user