Skip to content

Commit 05fe01a

Browse files
committed
First basic working app
1 parent b50502a commit 05fe01a

File tree

9 files changed

+278
-42
lines changed

9 files changed

+278
-42
lines changed

README.md

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ It works as follows:
1111
result with the docspell android app. It will upload it to the
1212
configured url.
1313

14+
*Note, this is in very early state. I'm trying to get it into the
15+
f-droid store, until then you'd need to download from the release page
16+
and install manually.*
1417

1518
## Building
1619

@@ -22,11 +25,13 @@ gradle assembleRelease
2225

2326
Settings for signing must be made available.
2427

25-
## Helpers
2628

27-
- https://nixos.wiki/wiki/Android
28-
- https://developer.android.com/jetpack/androidx/releases/swiperefreshlayout
29-
- https://www.android-examples.com/android-swipe-down-to-refresh-recyclerview/
30-
- https://developer.android.com/studio/write/java8-support.html
31-
- https://stackoverflow.com/questions/31367599/how-to-update-recyclerview-adapter-data
32-
- https://zatackcoder.com/android-recyclerview-swipe-to-multiple-options/
29+
## Android 7.0 + TLS
30+
31+
If you run in issues regarding your docspell server and tls, check
32+
whether you use android 7.0. It might be
33+
[this](https://github.com/nextcloud/news-android/issues/567#issuecomment-309700308).
34+
35+
Summary: The `ssl_ecdh_curve secp384r1;` definition in nginx/apache is
36+
the problem. Replacing this with `ssl_ecdh_curve prime256v1;` solved
37+
it. Or, if possible, update android to 7.1+.

app/src/main/java/org/docspell/docspellshare/activity/ShareActivity.java

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
import android.content.Intent;
55
import android.net.Uri;
66
import android.os.Bundle;
7+
import android.os.Handler;
78
import android.util.Log;
9+
import android.view.View;
810
import android.widget.ProgressBar;
911
import android.widget.TextView;
1012

@@ -13,6 +15,7 @@
1315
import org.docspell.docspellshare.R;
1416
import org.docspell.docspellshare.data.UrlItem;
1517
import org.docspell.docspellshare.http.HttpRequest;
18+
import org.docspell.docspellshare.http.ProgressListener;
1619
import org.docspell.docspellshare.http.UploadManager;
1720
import org.docspell.docspellshare.util.DataStore;
1821
import org.docspell.docspellshare.util.Strings;
@@ -23,26 +26,51 @@
2326
public class ShareActivity extends AppCompatActivity {
2427

2528
private DataStore dataStore;
29+
private Handler handler = new Handler();
30+
31+
private final ProgressListener progressListener =
32+
new ProgressListener() {
33+
@Override
34+
public void onProgress(String name, long bytesWritten, long total) {
35+
handler.post(
36+
() -> {
37+
TextView label = findViewById(R.id.uploadFileField);
38+
label.setText(getString(R.string.uploadingLabel, name));
39+
40+
ProgressBar pb = findViewById(R.id.uploadProgress);
41+
if (total > 0) {
42+
Log.w("share", String.format("Progress %d/%d", bytesWritten, total));
43+
pb.setIndeterminate(false);
44+
pb.setMax((int) total);
45+
pb.setProgress((int) bytesWritten);
46+
} else {
47+
Log.w("share", "No total size: " + total);
48+
pb.setIndeterminate(true);
49+
}
50+
});
51+
}
52+
53+
@Override
54+
public void onFinish(int code) {
55+
handler.post(() -> finish());
56+
}
57+
58+
@Override
59+
public void onException(Exception error) {
60+
handler.post(
61+
() -> {
62+
TextView msg = findViewById(R.id.finishMessage);
63+
msg.setText(getString(R.string.uploadError, error.getMessage()));
64+
});
65+
}
66+
};
2667

2768
@Override
2869
protected void onCreate(Bundle savedInstanceState) {
2970
super.onCreate(savedInstanceState);
3071
this.dataStore = new DataStore(this);
3172
setContentView(R.layout.activity_share);
32-
UploadManager.getInstance()
33-
.setProgress(
34-
(name, progress) -> {
35-
TextView label = findViewById(R.id.uploadFileField);
36-
label.post(
37-
() -> {
38-
label.setText(name);
39-
});
40-
ProgressBar pb = findViewById(R.id.uploadProgress);
41-
pb.post(
42-
() -> {
43-
pb.setProgress(progress);
44-
});
45-
});
73+
UploadManager.getInstance().setProgress(progressListener);
4674

4775
Intent intent = getIntent();
4876
String action = intent.getAction();
@@ -54,7 +82,7 @@ protected void onCreate(Bundle savedInstanceState) {
5482
} else {
5583
Uri uri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
5684
if (uri != null) {
57-
handleFiles(Collections.singletonList(uri), type);
85+
handleFiles(Collections.singletonList(uri));
5886
}
5987
}
6088
} else if (Intent.ACTION_SEND_MULTIPLE.equals(action) && type != null) {
@@ -63,21 +91,21 @@ protected void onCreate(Bundle savedInstanceState) {
6391
} else {
6492
List<Uri> fileUris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
6593
if (fileUris != null) {
66-
handleFiles(fileUris, type);
94+
handleFiles(fileUris);
6795
}
6896
}
6997
} else {
7098
Log.i("missing", "handling action '" + action + "' / '" + type + "' not implemented");
7199
}
72100
}
73101

74-
void handleFiles(List<Uri> uris, String type) {
102+
void handleFiles(List<Uri> uris) {
75103
String url = dataStore.getDefaultUrl().map(UrlItem::getUrl).orElse(null);
76104
if (url != null) {
77105
HttpRequest.Builder req = HttpRequest.newBuilder().setUrl(url);
78106
ContentResolver resolver = getContentResolver();
79107
for (Uri uri : uris) {
80-
req.addFile(resolver, uri, type);
108+
req.addFile(resolver, uri);
81109
}
82110
UploadManager.getInstance().submit(req.build());
83111
}

app/src/main/java/org/docspell/docspellshare/http/HttpRequest.java

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import androidx.annotation.NonNull;
77

88
import org.docspell.docspellshare.data.Option;
9+
import org.docspell.docspellshare.util.Uris;
910

1011
import java.io.IOException;
1112
import java.io.InputStream;
@@ -20,13 +21,15 @@
2021
import okhttp3.OkHttpClient;
2122
import okhttp3.Request;
2223
import okhttp3.RequestBody;
24+
import okhttp3.Response;
2325
import okio.BufferedSink;
2426
import okio.Okio;
2527
import okio.Source;
2628

2729
import static org.docspell.docspellshare.util.Strings.requireNonEmpty;
2830

2931
public final class HttpRequest {
32+
private static final int CHUNK_SIZE = 16 * 1024;
3033
private static final OkHttpClient client =
3134
new OkHttpClient.Builder()
3235
.connectionSpecs(
@@ -36,6 +39,7 @@ public final class HttpRequest {
3639
ConnectionSpec.CLEARTEXT))
3740
.readTimeout(5, TimeUnit.MINUTES)
3841
.writeTimeout(5, TimeUnit.MINUTES)
42+
.socketFactory(new RestrictedSocketFactory(CHUNK_SIZE))
3943
.followRedirects(true)
4044
.followSslRedirects(true)
4145
.build();
@@ -48,14 +52,15 @@ private HttpRequest(String url, List<DataPart> data) {
4852
this.data = data;
4953
}
5054

51-
public void execute() throws IOException {
55+
public int execute(ProgressListener progressListener) throws IOException {
5256
MultipartBody.Builder body = new MultipartBody.Builder().setType(MultipartBody.FORM);
5357
for (DataPart dp : data) {
54-
body.addFormDataPart("file", dp.getName(), createPartBody(dp));
58+
body.addFormDataPart("file", dp.getName(), createPartBody(dp, progressListener));
5559
}
5660

5761
Request req = new Request.Builder().url(url).post(body.build()).build();
58-
client.newCall(req).execute();
62+
Response response = client.newCall(req).execute();
63+
return response.code();
5964
}
6065

6166
public static Builder newBuilder() {
@@ -66,7 +71,7 @@ public static class Builder {
6671
private final List<DataPart> parts = new ArrayList<>();
6772
private String url;
6873

69-
public Builder addFile(ContentResolver resolver, Uri data, String type) {
74+
public Builder addFile(ContentResolver resolver, Uri data) {
7075
parts.add(
7176
new DataPart() {
7277
@Override
@@ -81,7 +86,12 @@ public String getName() {
8186

8287
@Override
8388
public Option<String> getType() {
84-
return Option.of(type);
89+
return Option.ofNullable(resolver.getType(data));
90+
}
91+
92+
@Override
93+
public long getTotalSize() {
94+
return Uris.getFileSize(data, resolver);
8595
}
8696
});
8797
return this;
@@ -103,9 +113,12 @@ public interface DataPart {
103113
String getName();
104114

105115
Option<String> getType();
116+
117+
/** Return -1, if unknown. */
118+
long getTotalSize();
106119
}
107120

108-
private static RequestBody createPartBody(DataPart part) {
121+
private static RequestBody createPartBody(DataPart part, ProgressListener listener) {
109122
final String octetStream = "application/octet-stream";
110123
return new RequestBody() {
111124
@Override
@@ -114,11 +127,22 @@ public MediaType contentType() {
114127
return mt != null ? mt : MediaType.get(octetStream);
115128
}
116129

130+
@Override
131+
public long contentLength() {
132+
return part.getTotalSize();
133+
}
134+
117135
@Override
118136
public void writeTo(@NonNull BufferedSink sink) throws IOException {
119137
try (InputStream in = part.getData();
120138
Source source = Okio.source(in)) {
121-
sink.writeAll(source);
139+
long total = 0;
140+
long read;
141+
while ((read = source.read(sink.getBuffer(), CHUNK_SIZE)) != -1) {
142+
total += read;
143+
sink.flush();
144+
listener.onProgress(part.getName(), total, part.getTotalSize());
145+
}
122146
}
123147
}
124148
};
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package org.docspell.docspellshare.http;
2+
3+
public interface ProgressListener {
4+
ProgressListener NONE =
5+
new ProgressListener() {
6+
@Override
7+
public void onProgress(String name, long bytesWritten, long total) {}
8+
9+
@Override
10+
public void onFinish(int code) {}
11+
12+
@Override
13+
public void onException(Exception error) {}
14+
};
15+
16+
void onProgress(String name, long bytesWritten, long total);
17+
18+
void onFinish(int code);
19+
20+
void onException(Exception error);
21+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package org.docspell.docspellshare.http;
2+
3+
import android.util.Log;
4+
5+
import java.io.IOException;
6+
import java.net.InetAddress;
7+
import java.net.Socket;
8+
import java.net.SocketException;
9+
import java.net.UnknownHostException;
10+
11+
import javax.net.SocketFactory;
12+
13+
/**
14+
* <b>Restricted Socket Factory</b>
15+
*
16+
* <p>OkHttp buffers to the network interface but the network interface's default buffer size is
17+
* sometimes set very high e.g. 512Kb which makes tracking upload progress impossible as the upload
18+
* content is sitting in the network interface buffer waiting to be transmitted. Re:
19+
* https://github.com/square/okhttp/issues/1078
20+
*
21+
* <p>So here, we create socket factory that forces all sockets to have a restricted send buffer
22+
* size. So that further down the chain in OkHttps' RequestBody we can track the actual progress to
23+
* the nearest [sendBufferSize] unit.
24+
*
25+
* <p>Example usage with OkHttpClient 2.x:
26+
*
27+
* <pre>
28+
* okHttpClient.setSocketFactory(new RestrictedSocketFactory(16 * 1024));
29+
* </pre>
30+
*
31+
* <p>Example usage with OkHttpClient 3.x:
32+
*
33+
* <pre>
34+
* okHttpClientBuilder.socketFactory(new RestrictedSocketFactory(16 * 1024))
35+
* </pre>
36+
*
37+
* <p>Created by Simon Lightfoot <[email protected]> on 04/04/2016.
38+
* https://gist.github.com/slightfoot/00a26683ea68856ceb50e26c7d8a47d0
39+
*/
40+
public class RestrictedSocketFactory extends SocketFactory {
41+
private static final String TAG = RestrictedSocketFactory.class.getSimpleName();
42+
43+
private int mSendBufferSize;
44+
45+
public RestrictedSocketFactory(int sendBufferSize) {
46+
mSendBufferSize = sendBufferSize;
47+
try {
48+
Socket socket = new Socket();
49+
Log.w(
50+
TAG,
51+
String.format(
52+
"Changing SO_SNDBUF on new sockets from %d to %d.",
53+
socket.getSendBufferSize(), sendBufferSize));
54+
} catch (SocketException e) {
55+
//
56+
}
57+
}
58+
59+
@Override
60+
public Socket createSocket() throws IOException {
61+
return updateSendBufferSize(new Socket());
62+
}
63+
64+
@Override
65+
public Socket createSocket(String host, int port) throws IOException {
66+
return updateSendBufferSize(new Socket(host, port));
67+
}
68+
69+
@Override
70+
public Socket createSocket(String host, int port, InetAddress localHost, int localPort)
71+
throws IOException, UnknownHostException {
72+
return updateSendBufferSize(new Socket(host, port, localHost, localPort));
73+
}
74+
75+
@Override
76+
public Socket createSocket(InetAddress host, int port) throws IOException {
77+
return updateSendBufferSize(new Socket(host, port));
78+
}
79+
80+
@Override
81+
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort)
82+
throws IOException {
83+
return updateSendBufferSize(new Socket(address, port, localAddress, localPort));
84+
}
85+
86+
private Socket updateSendBufferSize(Socket socket) throws IOException {
87+
socket.setSendBufferSize(mSendBufferSize);
88+
return socket;
89+
}
90+
}

0 commit comments

Comments
 (0)