Merge branch 'nathan-bluetooth-swap' into nearby-peers-with-bluetooth
This commit is contained in:
commit
0a373de7a4
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/white">
|
||||||
|
|
||||||
|
</org.fdroid.fdroid.views.swap.BluetoothDeviceListView>
|
72
F-Droid/res/layout/swap_bluetooth_header.xml
Normal file
72
F-Droid/res/layout/swap_bluetooth_header.xml
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<?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"
|
||||||
|
android:background="@color/fdroid_blue"
|
||||||
|
>
|
||||||
|
|
||||||
|
<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="5dp"
|
||||||
|
android:textSize="24sp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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="5dp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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="10dp"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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="5dp"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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_below="@+id/text_description"
|
||||||
android:layout_centerHorizontal="true" />
|
android:layout_centerHorizontal="true" />
|
||||||
|
|
||||||
<!--
|
|
||||||
<Button style="@style/SwapTheme.Wizard.OptionButton"
|
<Button style="@style/SwapTheme.Wizard.OptionButton"
|
||||||
android:id="@+id/btn_bluetooth"
|
android:id="@+id/btn_bluetooth"
|
||||||
android:text="@string/swap_use_bluetooth"
|
android:text="@string/swap_use_bluetooth"
|
||||||
android:layout_alignParentBottom="true" />
|
android:layout_alignParentBottom="true" />
|
||||||
|
|
||||||
|
<!--
|
||||||
<Button style="@style/SwapTheme.Wizard.OptionButton"
|
<Button style="@style/SwapTheme.Wizard.OptionButton"
|
||||||
android:text="@string/swap_wifi_help"
|
android:text="@string/swap_wifi_help"
|
||||||
android:layout_above="@id/btn_bluetooth"
|
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>
|
@ -347,6 +347,11 @@
|
|||||||
<string name="wifi_warning_public">May work</string>
|
<string name="wifi_warning_public">May work</string>
|
||||||
<string name="wifi_warning_private">Promising</string>
|
<string name="wifi_warning_private">Promising</string>
|
||||||
<string name="wifi_warning_personal">Best bet</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="loading">Loading...</string>
|
<string name="loading">Loading...</string>
|
||||||
<string name="swap_attempt_install">TRY TO INSTALL</string>
|
<string name="swap_attempt_install">TRY TO INSTALL</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -43,6 +43,22 @@
|
|||||||
<item name="android:background">@color/white</item>
|
<item name="android:background">@color/white</item>
|
||||||
</style>
|
</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 name="SwapTheme.AppList" parent="AppThemeLight">
|
||||||
</style>
|
</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;
|
||||||
|
}
|
||||||
|
}
|
@ -115,6 +115,8 @@ public class SwapService extends Service {
|
|||||||
public static final int STEP_SUCCESS = 7;
|
public static final int STEP_SUCCESS = 7;
|
||||||
public static final int STEP_CONFIRM_SWAP = 8;
|
public static final int STEP_CONFIRM_SWAP = 8;
|
||||||
|
|
||||||
|
public static final int STEP_BLUETOOTH = 1000; // TODO: Remove this once nathans code is merged and the UI is migrated to the nearby peers screen.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Special view, that we don't really want to actually store against the
|
* Special view, that we don't really want to actually store against the
|
||||||
* {@link SwapService#step}. Rather, we use it for the purpose of specifying
|
* {@link SwapService#step}. Rather, we use it for the purpose of specifying
|
||||||
@ -260,7 +262,7 @@ public class SwapService extends Service {
|
|||||||
* This is the same as, e.g. {@link Context#getSystemService(String)}
|
* 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_CONNECTING, STEP_SUCCESS, STEP_CONFIRM_SWAP, STEP_INITIAL_LOADING})
|
STEP_CONNECTING, STEP_SUCCESS, STEP_CONFIRM_SWAP, STEP_INITIAL_LOADING, STEP_BLUETOOTH})
|
||||||
@Retention(RetentionPolicy.SOURCE)
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
public @interface SwapStep {}
|
public @interface SwapStep {}
|
||||||
|
|
||||||
|
@ -39,6 +39,8 @@ public class WebServerType extends SwapType {
|
|||||||
public void run() {
|
public void run() {
|
||||||
localHttpd = new LocalHTTPD(
|
localHttpd = new LocalHTTPD(
|
||||||
context,
|
context,
|
||||||
|
FDroidApp.ipAddressString,
|
||||||
|
FDroidApp.port,
|
||||||
context.getFilesDir(),
|
context.getFilesDir(),
|
||||||
Preferences.get().isLocalRepoHttpsEnabled());
|
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.
|
// we were interrupted before proceeding to the download.
|
||||||
throwExceptionIfInterrupted();
|
throwExceptionIfInterrupted();
|
||||||
|
|
||||||
copyInputToOutputStream(getInputStream());
|
// TODO: Check side effects of changing this second getInputStream() to input.
|
||||||
|
copyInputToOutputStream(input);
|
||||||
} finally {
|
} finally {
|
||||||
Utils.closeQuietly(outputStream);
|
Utils.closeQuietly(outputStream);
|
||||||
Utils.closeQuietly(input);
|
Utils.closeQuietly(input);
|
||||||
@ -173,12 +174,13 @@ public abstract class Downloader {
|
|||||||
int count = input.read(buffer);
|
int count = input.read(buffer);
|
||||||
throwExceptionIfInterrupted();
|
throwExceptionIfInterrupted();
|
||||||
|
|
||||||
bytesRead += count;
|
|
||||||
sendProgress(bytesRead, totalBytes);
|
|
||||||
if (count == -1) {
|
if (count == -1) {
|
||||||
Log.d(TAG, "Finished downloading from stream");
|
Log.d(TAG, "Finished downloading from stream");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bytesRead += count;
|
||||||
|
sendProgress(bytesRead, totalBytes);
|
||||||
outputStream.write(buffer, 0, count);
|
outputStream.write(buffer, 0, count);
|
||||||
}
|
}
|
||||||
outputStream.flush();
|
outputStream.flush();
|
||||||
|
@ -1,26 +1,37 @@
|
|||||||
package org.fdroid.fdroid.net;
|
package org.fdroid.fdroid.net;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
public class DownloaderFactory {
|
public class DownloaderFactory {
|
||||||
|
|
||||||
public static Downloader create(String url, Context context)
|
public static Downloader create(String url, Context context) throws IOException {
|
||||||
throws IOException {
|
Uri uri = Uri.parse(url);
|
||||||
if (isOnionAddress(url)) {
|
if (isBluetoothAddress(uri)) {
|
||||||
|
return new BluetoothDownloader(null, uri.getPath(), context);
|
||||||
|
} else if (isOnionAddress(url)) {
|
||||||
return new TorHttpDownloader(url, context);
|
return new TorHttpDownloader(url, context);
|
||||||
|
} else {
|
||||||
|
return new HttpDownloader(url, context);
|
||||||
}
|
}
|
||||||
return new HttpDownloader(url, context);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Downloader create(String url, File destFile)
|
public static Downloader create(String url, File destFile) throws IOException {
|
||||||
throws IOException {
|
Uri uri = Uri.parse(url);
|
||||||
if (isOnionAddress(url)) {
|
if (isBluetoothAddress(uri)) {
|
||||||
|
return new BluetoothDownloader(null, uri.getPath(), destFile);
|
||||||
|
} else if (isOnionAddress(url)) {
|
||||||
return new TorHttpDownloader(url, destFile);
|
return new TorHttpDownloader(url, destFile);
|
||||||
|
} else {
|
||||||
|
return new HttpDownloader(url, destFile);
|
||||||
}
|
}
|
||||||
return new HttpDownloader(url, destFile);
|
}
|
||||||
|
|
||||||
|
private static boolean isBluetoothAddress(Uri uri) {
|
||||||
|
return "bluetooth".equalsIgnoreCase(uri.getScheme());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isOnionAddress(String url) {
|
private static boolean isOnionAddress(String url) {
|
||||||
|
@ -2,9 +2,9 @@ package org.fdroid.fdroid.net;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import org.fdroid.fdroid.Preferences;
|
import org.fdroid.fdroid.Preferences;
|
||||||
|
|
||||||
|
import javax.net.ssl.SSLHandshakeException;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -16,8 +16,6 @@ import java.net.Proxy;
|
|||||||
import java.net.SocketAddress;
|
import java.net.SocketAddress;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
|
||||||
import javax.net.ssl.SSLHandshakeException;
|
|
||||||
|
|
||||||
public class HttpDownloader extends Downloader {
|
public class HttpDownloader extends Downloader {
|
||||||
private static final String TAG = "HttpDownloader";
|
private static final String TAG = "HttpDownloader";
|
||||||
|
|
||||||
@ -26,6 +24,7 @@ public class HttpDownloader extends Downloader {
|
|||||||
|
|
||||||
protected HttpURLConnection connection;
|
protected HttpURLConnection connection;
|
||||||
private int statusCode = -1;
|
private int statusCode = -1;
|
||||||
|
private boolean onlyStream = false;
|
||||||
|
|
||||||
// The context is required for opening the file to write to.
|
// The context is required for opening the file to write to.
|
||||||
HttpDownloader(String source, File destFile)
|
HttpDownloader(String source, File destFile)
|
||||||
@ -39,11 +38,22 @@ public class HttpDownloader extends Downloader {
|
|||||||
* you are done*.
|
* you are done*.
|
||||||
* @see org.fdroid.fdroid.net.Downloader#getFile()
|
* @see org.fdroid.fdroid.net.Downloader#getFile()
|
||||||
*/
|
*/
|
||||||
HttpDownloader(String source, Context ctx) throws IOException {
|
public HttpDownloader(String source, Context ctx) throws IOException {
|
||||||
super(ctx);
|
super(ctx);
|
||||||
sourceUrl = new URL(source);
|
sourceUrl = new URL(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calling this makes this downloader not download a file. Instead, it will
|
||||||
|
* only stream the file through the {@link HttpDownloader#getInputStream()}
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public HttpDownloader streamDontDownload()
|
||||||
|
{
|
||||||
|
onlyStream = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public InputStream getInputStream() throws IOException {
|
public InputStream getInputStream() throws IOException {
|
||||||
setupConnection();
|
setupConnection();
|
||||||
@ -134,4 +144,8 @@ public class HttpDownloader extends Downloader {
|
|||||||
return this.statusCode != 304;
|
return this.statusCode != 304;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getStatusCode() {
|
||||||
|
return statusCode;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -36,8 +36,8 @@ public class LocalHTTPD extends NanoHTTPD {
|
|||||||
private final File webRoot;
|
private final File webRoot;
|
||||||
private final boolean logRequests;
|
private final boolean logRequests;
|
||||||
|
|
||||||
public LocalHTTPD(Context context, File webRoot, boolean useHttps) {
|
public LocalHTTPD(Context context, String hostname, int port, File webRoot, boolean useHttps) {
|
||||||
super(FDroidApp.ipAddressString, FDroidApp.port);
|
super(hostname, port);
|
||||||
this.logRequests = false;
|
this.logRequests = false;
|
||||||
this.webRoot = webRoot;
|
this.webRoot = webRoot;
|
||||||
this.context = context.getApplicationContext();
|
this.context = context.getApplicationContext();
|
||||||
|
@ -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.createInsecureRfcommSocketToServiceRecord(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");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
370
F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java
Normal file
370
F-Droid/src/org/fdroid/fdroid/net/bluetooth/BluetoothServer.java
Normal file
@ -0,0 +1,370 @@
|
|||||||
|
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 android.webkit.MimeTypeMap;
|
||||||
|
|
||||||
|
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.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FilenameFilter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import fi.iki.elonen.NanoHTTPD;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
private String deviceBluetoothName = null;
|
||||||
|
public final static String BLUETOOTH_NAME_TAG = "FDroid:";
|
||||||
|
private final File webRoot;
|
||||||
|
|
||||||
|
public BluetoothServer(Context context, File webRoot) {
|
||||||
|
this.context = context.getApplicationContext();
|
||||||
|
this.webRoot = webRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close() {
|
||||||
|
|
||||||
|
for (Connection connection : clients) {
|
||||||
|
connection.interrupt();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serverSocket != null) {
|
||||||
|
Utils.closeQuietly(serverSocket);
|
||||||
|
}
|
||||||
|
|
||||||
|
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
|
||||||
|
adapter.setName(deviceBluetoothName.replace(BLUETOOTH_NAME_TAG,""));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
|
||||||
|
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
|
||||||
|
|
||||||
|
|
||||||
|
//store the original bluetoothname, and update this one to be unique
|
||||||
|
deviceBluetoothName = adapter.getName();
|
||||||
|
|
||||||
|
if (!deviceBluetoothName.contains(BLUETOOTH_NAME_TAG))
|
||||||
|
adapter.setName(BLUETOOTH_NAME_TAG + deviceBluetoothName);
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
serverSocket = adapter.listenUsingInsecureRfcommWithServiceRecord("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, webRoot);
|
||||||
|
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;
|
||||||
|
private final File webRoot;
|
||||||
|
|
||||||
|
public Connection(Context context, BluetoothSocket socket, File webRoot) {
|
||||||
|
this.context = context.getApplicationContext();
|
||||||
|
this.socket = socket;
|
||||||
|
this.webRoot = webRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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.");
|
||||||
|
|
||||||
|
Response.Builder builder = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// HttpDownloader downloader = new HttpDownloader("http://127.0.0.1:" + ( FDroidApp.port) + "/" + request.getPath(), context);
|
||||||
|
int statusCode = 404;
|
||||||
|
int totalSize = -1;
|
||||||
|
|
||||||
|
if (request.getMethod().equals(Request.Methods.HEAD)) {
|
||||||
|
builder = new Response.Builder();
|
||||||
|
} else {
|
||||||
|
HashMap<String, String> headers = new HashMap<String, String>();
|
||||||
|
Response resp = respond(headers, "/" + request.getPath());
|
||||||
|
|
||||||
|
builder = new Response.Builder(resp.toContentStream());
|
||||||
|
statusCode = resp.getStatusCode();
|
||||||
|
totalSize = resp.getFileSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(statusCode)
|
||||||
|
.setFileSize(totalSize)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
} catch (Exception 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);
|
||||||
|
}*/
|
||||||
|
|
||||||
|
Log.e(TAG, "error processing request; sending 500 response", e);
|
||||||
|
|
||||||
|
if (builder == null)
|
||||||
|
builder = new Response.Builder();
|
||||||
|
|
||||||
|
return builder
|
||||||
|
.setStatusCode(500)
|
||||||
|
.setFileSize(0)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private Response respond(Map<String, String> headers, String uri) {
|
||||||
|
// Remove URL arguments
|
||||||
|
uri = uri.trim().replace(File.separatorChar, '/');
|
||||||
|
if (uri.indexOf('?') >= 0) {
|
||||||
|
uri = uri.substring(0, uri.indexOf('?'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prohibit getting out of current directory
|
||||||
|
if (uri.contains("../")) {
|
||||||
|
return createResponse(NanoHTTPD.Response.Status.FORBIDDEN, NanoHTTPD.MIME_PLAINTEXT,
|
||||||
|
"FORBIDDEN: Won't serve ../ for security reasons.");
|
||||||
|
}
|
||||||
|
|
||||||
|
File f = new File(webRoot, uri);
|
||||||
|
if (!f.exists()) {
|
||||||
|
return createResponse(NanoHTTPD.Response.Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT,
|
||||||
|
"Error 404, file not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Browsers get confused without '/' after the directory, send a
|
||||||
|
// redirect.
|
||||||
|
if (f.isDirectory() && !uri.endsWith("/")) {
|
||||||
|
uri += "/";
|
||||||
|
Response res = createResponse(NanoHTTPD.Response.Status.REDIRECT, NanoHTTPD.MIME_HTML,
|
||||||
|
"<html><body>Redirected: <a href=\"" +
|
||||||
|
uri + "\">" + uri + "</a></body></html>");
|
||||||
|
res.addHeader("Location", uri);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (f.isDirectory()) {
|
||||||
|
// First look for index files (index.html, index.htm, etc) and if
|
||||||
|
// none found, list the directory if readable.
|
||||||
|
String indexFile = findIndexFileInDirectory(f);
|
||||||
|
if (indexFile == null) {
|
||||||
|
if (f.canRead()) {
|
||||||
|
// No index file, list the directory if it is readable
|
||||||
|
return createResponse(NanoHTTPD.Response.Status.NOT_FOUND, NanoHTTPD.MIME_HTML, "");
|
||||||
|
} else {
|
||||||
|
return createResponse(NanoHTTPD.Response.Status.FORBIDDEN, NanoHTTPD.MIME_PLAINTEXT,
|
||||||
|
"FORBIDDEN: No directory listing.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return respond(headers, uri + indexFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Response response = serveFile(uri, headers, f, getMimeTypeForFile(uri));
|
||||||
|
return response != null ? response :
|
||||||
|
createResponse(NanoHTTPD.Response.Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT,
|
||||||
|
"Error 404, file not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serves file from homeDir and its' subdirectories (only). Uses only URI,
|
||||||
|
* ignores all headers and HTTP parameters.
|
||||||
|
*/
|
||||||
|
Response serveFile(String uri, Map<String, String> header, File file, String mime) {
|
||||||
|
Response res;
|
||||||
|
try {
|
||||||
|
// Calculate etag
|
||||||
|
String etag = Integer
|
||||||
|
.toHexString((file.getAbsolutePath() + file.lastModified() + "" + file.length())
|
||||||
|
.hashCode());
|
||||||
|
|
||||||
|
// Support (simple) skipping:
|
||||||
|
long startFrom = 0;
|
||||||
|
long endAt = -1;
|
||||||
|
String range = header.get("range");
|
||||||
|
if (range != null) {
|
||||||
|
if (range.startsWith("bytes=")) {
|
||||||
|
range = range.substring("bytes=".length());
|
||||||
|
int minus = range.indexOf('-');
|
||||||
|
try {
|
||||||
|
if (minus > 0) {
|
||||||
|
startFrom = Long.parseLong(range.substring(0, minus));
|
||||||
|
endAt = Long.parseLong(range.substring(minus + 1));
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change return code and add Content-Range header when skipping is
|
||||||
|
// requested
|
||||||
|
long fileLen = file.length();
|
||||||
|
if (range != null && startFrom >= 0) {
|
||||||
|
if (startFrom >= fileLen) {
|
||||||
|
res = createResponse(NanoHTTPD.Response.Status.RANGE_NOT_SATISFIABLE,
|
||||||
|
NanoHTTPD.MIME_PLAINTEXT, "");
|
||||||
|
res.addHeader("Content-Range", "bytes 0-0/" + fileLen);
|
||||||
|
res.addHeader("ETag", etag);
|
||||||
|
} else {
|
||||||
|
if (endAt < 0) {
|
||||||
|
endAt = fileLen - 1;
|
||||||
|
}
|
||||||
|
long newLen = endAt - startFrom + 1;
|
||||||
|
if (newLen < 0) {
|
||||||
|
newLen = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
final long dataLen = newLen;
|
||||||
|
FileInputStream fis = new FileInputStream(file) {
|
||||||
|
@Override
|
||||||
|
public int available() throws IOException {
|
||||||
|
return (int) dataLen;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fis.skip(startFrom);
|
||||||
|
|
||||||
|
res = createResponse(NanoHTTPD.Response.Status.PARTIAL_CONTENT, mime, fis);
|
||||||
|
res.addHeader("Content-Length", "" + dataLen);
|
||||||
|
res.addHeader("Content-Range", "bytes " + startFrom + "-" + endAt + "/"
|
||||||
|
+ fileLen);
|
||||||
|
res.addHeader("ETag", etag);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (etag.equals(header.get("if-none-match")))
|
||||||
|
res = createResponse(NanoHTTPD.Response.Status.NOT_MODIFIED, mime, "");
|
||||||
|
else {
|
||||||
|
res = createResponse(NanoHTTPD.Response.Status.OK, mime, new FileInputStream(file));
|
||||||
|
res.addHeader("Content-Length", "" + fileLen);
|
||||||
|
res.addHeader("ETag", etag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
res = createResponse(NanoHTTPD.Response.Status.FORBIDDEN, NanoHTTPD.MIME_PLAINTEXT,
|
||||||
|
"FORBIDDEN: Reading file failed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Announce that the file server accepts partial content requests
|
||||||
|
private Response createResponse(NanoHTTPD.Response.Status status, String mimeType, String content) {
|
||||||
|
Response res = new Response(status.getRequestStatus(), mimeType, content);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Announce that the file server accepts partial content requests
|
||||||
|
private Response createResponse(NanoHTTPD.Response.Status status, String mimeType, InputStream content) {
|
||||||
|
Response res = new Response(status.getRequestStatus(), mimeType, content);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getMimeTypeForFile(String uri) {
|
||||||
|
String type = null;
|
||||||
|
String extension = MimeTypeMap.getFileExtensionFromUrl(uri);
|
||||||
|
if (extension != null) {
|
||||||
|
MimeTypeMap mime = MimeTypeMap.getSingleton();
|
||||||
|
type = mime.getMimeTypeFromExtension(extension);
|
||||||
|
}
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String findIndexFileInDirectory(File directory) {
|
||||||
|
String indexFileName = "index.html";
|
||||||
|
File indexFile = new File(directory, indexFileName);
|
||||||
|
if (indexFile.exists()) {
|
||||||
|
return indexFileName;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
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,179 @@
|
|||||||
|
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.StringBufferInputStream;
|
||||||
|
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 Response(int statusCode, String mimeType, String content) {
|
||||||
|
this.statusCode = statusCode;
|
||||||
|
this.headers = new HashMap<String,String>();
|
||||||
|
this.contentStream = new StringBufferInputStream(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Response(int statusCode, String mimeType, InputStream contentStream) {
|
||||||
|
this.statusCode = statusCode;
|
||||||
|
this.headers = new HashMap<String,String>();
|
||||||
|
this.contentStream = contentStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addHeader (String key, String value)
|
||||||
|
{
|
||||||
|
headers.put(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected View getHeaderView(LayoutInflater inflater, ViewGroup container) {
|
protected View getHeaderView() {
|
||||||
|
return headerView;
|
||||||
|
}
|
||||||
|
|
||||||
|
private View headerView = null;
|
||||||
|
|
||||||
|
private View getHeaderView(LayoutInflater inflater, ViewGroup container) {
|
||||||
if (getHeaderLayout() > 0) {
|
if (getHeaderLayout() > 0) {
|
||||||
return inflater.inflate(getHeaderLayout(), null, false);
|
if (headerView == null) {
|
||||||
|
headerView = inflater.inflate(getHeaderLayout(), null, false);
|
||||||
|
}
|
||||||
|
return headerView;
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,376 @@
|
|||||||
|
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 android.widget.Toast;
|
||||||
|
|
||||||
|
import org.fdroid.fdroid.R;
|
||||||
|
import org.fdroid.fdroid.localrepo.SwapService;
|
||||||
|
import org.fdroid.fdroid.net.BluetoothDownloader;
|
||||||
|
import org.fdroid.fdroid.net.bluetooth.BluetoothClient;
|
||||||
|
import org.fdroid.fdroid.net.bluetooth.BluetoothConnection;
|
||||||
|
import org.fdroid.fdroid.net.bluetooth.BluetoothServer;
|
||||||
|
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;
|
||||||
|
|
||||||
|
private boolean firstScan = true;
|
||||||
|
|
||||||
|
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 SwapService.STEP_BLUETOOTH;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPreviousStep() {
|
||||||
|
return SwapService.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);
|
||||||
|
setOnItemClickListener(this);
|
||||||
|
|
||||||
|
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());
|
||||||
|
|
||||||
|
initiateBluetoothScan();
|
||||||
|
|
||||||
|
// 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...");
|
||||||
|
|
||||||
|
if (cancelMenuItem != null) {
|
||||||
|
cancelMenuItem.setVisible(true);
|
||||||
|
scanMenuItem.setVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
final ContentLoadingProgressBar loadingBar = getLoadingIndicator();
|
||||||
|
|
||||||
|
loadingBar.show();
|
||||||
|
|
||||||
|
final BluetoothAdapter bluetooth = BluetoothAdapter.getDefaultAdapter();
|
||||||
|
|
||||||
|
if (firstScan) {
|
||||||
|
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());
|
||||||
|
|
||||||
|
if (device != null && device.getName() != null)
|
||||||
|
if (device.getName().contains(BluetoothServer.BLUETOOTH_NAME_TAG)) {
|
||||||
|
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));
|
||||||
|
|
||||||
|
firstScan = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (bluetooth.isDiscovering())
|
||||||
|
{
|
||||||
|
bluetooth.cancelDiscovery();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bluetooth.startDiscovery()) {
|
||||||
|
// TODO: Discovery did not start for some reason :(
|
||||||
|
Log.e(TAG, "Could not start bluetooth discovery, but am not sure why :(");
|
||||||
|
Toast.makeText(getContext(),"There was a problem looking for Bluetooth devices",Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.MenuInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.widget.Button;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.RelativeLayout;
|
import android.widget.RelativeLayout;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
@ -72,6 +73,15 @@ public class JoinWifiView extends RelativeLayout implements SwapWorkflowActivity
|
|||||||
new IntentFilter(WifiStateChangeService.BROADCAST)
|
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.
|
// TODO: Listen for "Connecting..." state and reflect that in the view too.
|
||||||
@ -117,7 +127,7 @@ public class JoinWifiView extends RelativeLayout implements SwapWorkflowActivity
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -3,6 +3,7 @@ package org.fdroid.fdroid.views.swap;
|
|||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.ProgressDialog;
|
import android.app.ProgressDialog;
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
|
import android.bluetooth.BluetoothAdapter;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.ServiceConnection;
|
import android.content.ServiceConnection;
|
||||||
@ -38,6 +39,7 @@ import org.fdroid.fdroid.data.NewRepoConfig;
|
|||||||
import org.fdroid.fdroid.localrepo.LocalRepoManager;
|
import org.fdroid.fdroid.localrepo.LocalRepoManager;
|
||||||
import org.fdroid.fdroid.localrepo.SwapService;
|
import org.fdroid.fdroid.localrepo.SwapService;
|
||||||
import org.fdroid.fdroid.localrepo.peers.Peer;
|
import org.fdroid.fdroid.localrepo.peers.Peer;
|
||||||
|
import org.fdroid.fdroid.net.bluetooth.BluetoothServer;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
@ -84,7 +86,10 @@ public class SwapWorkflowActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static final String TAG = "SwapWorkflowActivity";
|
private static final String TAG = "SwapWorkflowActivity";
|
||||||
|
|
||||||
private static final int CONNECT_TO_SWAP = 1;
|
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 Toolbar toolbar;
|
||||||
private InnerView currentView;
|
private InnerView currentView;
|
||||||
@ -344,6 +349,10 @@ public class SwapWorkflowActivity extends AppCompatActivity {
|
|||||||
inflateInnerView(R.layout.swap_join_wifi);
|
inflateInnerView(R.layout.swap_join_wifi);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void showBluetoothDeviceList() {
|
||||||
|
inflateInnerView(R.layout.swap_bluetooth_devices);
|
||||||
|
}
|
||||||
|
|
||||||
public void showWifiQr() {
|
public void showWifiQr() {
|
||||||
inflateInnerView(R.layout.swap_wifi_qr);
|
inflateInnerView(R.layout.swap_wifi_qr);
|
||||||
}
|
}
|
||||||
@ -408,9 +417,88 @@ public class SwapWorkflowActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
} else if (requestCode == CONNECT_TO_SWAP && resultCode == Activity.RESULT_OK) {
|
} else if (requestCode == CONNECT_TO_SWAP && resultCode == Activity.RESULT_OK) {
|
||||||
finish();
|
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();
|
||||||
|
|
||||||
|
if (adapter != null)
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "Staring the Bluetooth Server whether we are discoverable or not, since paired devices can still connect.");
|
||||||
|
startBluetoothServer();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startBluetoothServer() {
|
||||||
|
Log.d(TAG, "Starting bluetooth server.");
|
||||||
|
if (service == null) {
|
||||||
|
throw new IllegalStateException("We are attempting to do bluetooth stuff, but the service is not ready.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!service.isEnabled()) {
|
||||||
|
service.enableSwapping();
|
||||||
|
}
|
||||||
|
|
||||||
|
new BluetoothServer(this,getFilesDir()).start();
|
||||||
|
showBluetoothDeviceList();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class PrepareInitialSwapRepo extends PrepareSwapRepo {
|
class PrepareInitialSwapRepo extends PrepareSwapRepo {
|
||||||
public PrepareInitialSwapRepo() {
|
public PrepareInitialSwapRepo() {
|
||||||
super(new HashSet<>(Arrays.asList(new String[] { "org.fdroid.fdroid" })));
|
super(new HashSet<>(Arrays.asList(new String[] { "org.fdroid.fdroid" })));
|
||||||
|
Loading…
x
Reference in New Issue
Block a user