Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Http attachment improvements #675

Merged
merged 3 commits into from
Jul 2, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion acra-core/src/main/java/org/acra/annotation/AcraCore.java
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@
* </p>
* Side effects:
* <ul>
* <li>POST mode: requests will be sent with content-type multipart/mixed</li>
* <li>POST mode: requests will be sent with content-type multipart/form-data</li>
* <li>PUT mode: There will be additional requests with the attachments. Naming scheme: [report-id]-[filename]</li>
* <li>EMAIL mode: Some email clients do not support attachments, so some email may lack these attachments. Note that attachments might be readable to email clients when they are sent.</li>
* </ul>
Expand Down
6 changes: 2 additions & 4 deletions acra-http/src/main/java/org/acra/http/BaseHttpRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,6 @@ protected void configureHeaders(@NonNull HttpURLConnection connection, @Nullable

@SuppressWarnings("WeakerAccess")
protected void writeContent(@NonNull HttpURLConnection connection, @NonNull Method method, @NonNull T content) throws IOException {
final byte[] contentAsBytes = asBytes(content);
// write output - see http://developer.android.com/reference/java/net/HttpURLConnection.html
connection.setRequestMethod(method.name());
connection.setDoOutput(true);
Expand All @@ -187,15 +186,14 @@ protected void writeContent(@NonNull HttpURLConnection connection, @NonNull Meth
final OutputStream outputStream = senderConfiguration.compress() ? new GZIPOutputStream(connection.getOutputStream(), ACRAConstants.DEFAULT_BUFFER_SIZE_IN_BYTES)
: new BufferedOutputStream(connection.getOutputStream());
try {
outputStream.write(contentAsBytes);
write(outputStream, content);
outputStream.flush();
} finally {
IOUtils.safeClose(outputStream);
}
}

@NonNull
protected abstract byte[] asBytes(@NonNull T content) throws IOException;
protected abstract void write(OutputStream outputStream, @NonNull T content) throws IOException;

@SuppressWarnings("WeakerAccess")
protected void handleResponse(int responseCode, String responseMessage) throws IOException {
Expand Down
6 changes: 3 additions & 3 deletions acra-http/src/main/java/org/acra/http/BinaryHttpRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import org.acra.util.UriUtils;

import java.io.IOException;
import java.io.OutputStream;
import java.util.Map;

/**
Expand All @@ -48,9 +49,8 @@ protected String getContentType(@NonNull Context context, @NonNull Uri uri) {
return UriUtils.getMimeType(context, uri);
}

@NonNull
@Override
protected byte[] asBytes(@NonNull Uri content) throws IOException {
return UriUtils.uriToByteArray(context, content);
protected void write(OutputStream outputStream, @NonNull Uri content) throws IOException {
UriUtils.copyFromUri(context, outputStream, content);
}
}
6 changes: 3 additions & 3 deletions acra-http/src/main/java/org/acra/http/DefaultHttpRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import org.acra.sender.HttpSender;

import java.io.IOException;
import java.io.OutputStream;
import java.util.Map;

/**
Expand All @@ -46,9 +47,8 @@ protected String getContentType(@NonNull Context context, @NonNull String s) {
return contentType;
}

@NonNull
@Override
protected byte[] asBytes(@NonNull String content) throws IOException {
return content.getBytes(ACRAConstants.UTF8);
protected void write(OutputStream outputStream, @NonNull String content) throws IOException {
outputStream.write(content.getBytes(ACRAConstants.UTF8));
}
}
55 changes: 27 additions & 28 deletions acra-http/src/main/java/org/acra/http/MultipartHttpRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,18 @@
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Pair;

import org.acra.ACRA;
import org.acra.ACRAConstants;
import org.acra.config.CoreConfiguration;
import org.acra.sender.HttpSender;
import org.acra.util.UriUtils;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.io.*;
import java.util.List;
import java.util.Map;

/**
* Produces <a href="https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html">RFC 1341</a> compliant requests
* Produces <a href="https://tools.ietf.org/html/rfc7578">RFC 7578</a> compliant requests
*
* @author F43nd1r
* @since 11.03.2017
Expand All @@ -46,7 +43,10 @@ public class MultipartHttpRequest extends BaseHttpRequest<Pair<String, List<Uri>
private static final String BOUNDARY = "%&ACRA_REPORT_DIVIDER&%";
private static final String BOUNDARY_FIX = "--";
private static final String NEW_LINE = "\r\n";
private static final String CONTENT_TYPE = "Content-Type: ";
private static final String SECTION_START = NEW_LINE + BOUNDARY_FIX + BOUNDARY + NEW_LINE;
private static final String MESSAGE_END = NEW_LINE + BOUNDARY_FIX + BOUNDARY + BOUNDARY_FIX + NEW_LINE;
private static final String CONTENT_DISPOSITION = "Content-Disposition: form-data; name=\"%s\"; filename=\"%s\"" + NEW_LINE;
private static final String CONTENT_TYPE = "Content-Type: %s" + NEW_LINE;
private final Context context;
private final String contentType;

Expand All @@ -60,31 +60,30 @@ public MultipartHttpRequest(@NonNull CoreConfiguration config, @NonNull Context
@NonNull
@Override
protected String getContentType(@NonNull Context context, @NonNull Pair<String, List<Uri>> stringListPair) {
return "multipart/mixed; boundary=" + BOUNDARY;
return "multipart/form-data; boundary=" + BOUNDARY;
}

@NonNull
@Override
protected byte[] asBytes(@NonNull Pair<String, List<Uri>> content) throws IOException {
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
final Writer writer = new OutputStreamWriter(outputStream, ACRAConstants.UTF8);
//noinspection TryFinallyCanBeTryWithResources we do not target api 19
try {
writer.append(NEW_LINE).append(BOUNDARY_FIX).append(BOUNDARY).append(NEW_LINE);
writer.append(CONTENT_TYPE).append(contentType).append(NEW_LINE).append(NEW_LINE);
writer.append(content.first);
for (Uri uri : content.second) {
writer.append(NEW_LINE).append(BOUNDARY_FIX).append(BOUNDARY).append(NEW_LINE);
writer.append("Content-Disposition: attachment; filename=\"").append(UriUtils.getFileNameFromUri(context, uri)).append('"').append(NEW_LINE);
writer.append(CONTENT_TYPE).append(UriUtils.getMimeType(context, uri)).append(NEW_LINE).append(NEW_LINE);
writer.flush();
outputStream.write(UriUtils.uriToByteArray(context, uri));
protected void write(OutputStream outputStream, @NonNull Pair<String, List<Uri>> content) throws IOException {
final PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream, ACRAConstants.UTF8));
writer.append(SECTION_START)
.format(CONTENT_DISPOSITION, "ACRA_REPORT", "")
.format(CONTENT_TYPE, contentType)
.append(NEW_LINE)
.append(content.first);
for (Uri uri : content.second) {
try {
String name = UriUtils.getFileNameFromUri(context, uri);
writer.append(SECTION_START)
.format(CONTENT_DISPOSITION, "ACRA_ATTACHMENT", name)
.format(CONTENT_TYPE, UriUtils.getMimeType(context, uri))
.append(NEW_LINE)
.flush();
UriUtils.copyFromUri(context, outputStream, uri);
} catch (FileNotFoundException e) {
ACRA.log.w("Not sending attachment", e);
}
writer.append(NEW_LINE).append(BOUNDARY_FIX).append(BOUNDARY).append(BOUNDARY_FIX).append(NEW_LINE);
writer.flush();
return outputStream.toByteArray();
} finally {
writer.close();
}
writer.append(MESSAGE_END).flush();
}
}
58 changes: 31 additions & 27 deletions acra-http/src/main/java/org/acra/sender/HttpSender.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Pair;

import org.acra.ACRA;
import org.acra.ACRAConstants;
import org.acra.ReportField;
Expand All @@ -36,6 +35,7 @@
import org.acra.util.InstanceCreator;
import org.acra.util.UriUtils;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
Expand All @@ -51,30 +51,6 @@
*/
public class HttpSender implements ReportSender {

/**
* Available HTTP methods to send data. Only POST and PUT are currently
* supported.
*/
public enum Method {
POST {
@NonNull
@Override
URL createURL(@NonNull String baseUrl, @NonNull CrashReportData report) throws MalformedURLException {
return new URL(baseUrl);
}
},
PUT {
@NonNull
@Override
URL createURL(@NonNull String baseUrl, @NonNull CrashReportData report) throws MalformedURLException {
return new URL(baseUrl + '/' + report.getString(ReportField.REPORT_ID));
}
};

@NonNull
abstract URL createURL(@NonNull String baseUrl, @NonNull CrashReportData report) throws MalformedURLException;
}

private final CoreConfiguration config;
private final HttpSenderConfiguration httpConfig;
private final Uri mFormUri;
Expand Down Expand Up @@ -201,8 +177,12 @@ protected void postMultipart(@NonNull CoreConfiguration configuration, @NonNull
protected void putAttachment(@NonNull CoreConfiguration configuration, @NonNull Context context,
@Nullable String login, @Nullable String password, int connectionTimeOut, int socketTimeOut, @Nullable Map<String, String> headers,
@NonNull URL url, @NonNull Uri attachment) throws IOException {
final URL attachmentUrl = new URL(url.toString() + "-" + UriUtils.getFileNameFromUri(context, attachment));
new BinaryHttpRequest(configuration, context, login, password, connectionTimeOut, socketTimeOut, headers).send(attachmentUrl, attachment);
try {
final URL attachmentUrl = new URL(url.toString() + "-" + UriUtils.getFileNameFromUri(context, attachment));
new BinaryHttpRequest(configuration, context, login, password, connectionTimeOut, socketTimeOut, headers).send(attachmentUrl, attachment);
} catch (FileNotFoundException e) {
ACRA.log.w("Not sending attachment", e);
}
}

/**
Expand All @@ -223,4 +203,28 @@ private boolean isNull(@Nullable String aString) {
return aString == null || ACRAConstants.NULL_VALUE.equals(aString);
}

/**
* Available HTTP methods to send data. Only POST and PUT are currently
* supported.
*/
public enum Method {
POST {
@NonNull
@Override
URL createURL(@NonNull String baseUrl, @NonNull CrashReportData report) throws MalformedURLException {
return new URL(baseUrl);
}
},
PUT {
@NonNull
@Override
URL createURL(@NonNull String baseUrl, @NonNull CrashReportData report) throws MalformedURLException {
return new URL(baseUrl + '/' + report.getString(ReportField.REPORT_ID));
}
};

@NonNull
abstract URL createURL(@NonNull String baseUrl, @NonNull CrashReportData report) throws MalformedURLException;
}

}
50 changes: 16 additions & 34 deletions acra-http/src/main/java/org/acra/util/UriUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,13 @@
import android.net.Uri;
import android.provider.OpenableColumns;
import android.support.annotation.NonNull;

import org.acra.ACRAConstants;
import org.acra.attachment.AcraContentProvider;

import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

/**
* @author F43nd1r
Expand All @@ -40,44 +39,27 @@ public final class UriUtils {
private UriUtils() {
}

@NonNull
public static byte[] uriToByteArray(@NonNull Context context, @NonNull Uri uri) throws IOException {
final InputStream inputStream = context.getContentResolver().openInputStream(uri);
if (inputStream == null) {
throw new FileNotFoundException("Could not open " + uri.toString());
}
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
final byte[] buffer = new byte[ACRAConstants.DEFAULT_BUFFER_SIZE_IN_BYTES];
int length;
while ((length = inputStream.read(buffer)) > 0) {
outputStream.write(buffer, 0, length);
public static void copyFromUri(@NonNull Context context, @NonNull OutputStream outputStream, @NonNull Uri uri) throws IOException {
try (final InputStream inputStream = context.getContentResolver().openInputStream(uri)) {
if (inputStream == null) {
throw new FileNotFoundException("Could not open " + uri.toString());
}
final byte[] buffer = new byte[ACRAConstants.DEFAULT_BUFFER_SIZE_IN_BYTES];
int length;
while ((length = inputStream.read(buffer)) > 0) {
outputStream.write(buffer, 0, length);
}
}
return outputStream.toByteArray();
}

@NonNull
public static String getFileNameFromUri(@NonNull Context context, @NonNull Uri uri) {
String result = null;
if (uri.getScheme().equals(ContentResolver.SCHEME_CONTENT)) {
final Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
try {
if (cursor != null && cursor.moveToFirst()) {
result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
}
} finally {
if (cursor != null) {
cursor.close();
}
}
}
if (result == null) {
result = uri.getPath();
final int cut = result.lastIndexOf('/');
if (cut != -1) {
result = result.substring(cut + 1);
public static String getFileNameFromUri(@NonNull Context context, @NonNull Uri uri) throws FileNotFoundException {
try (Cursor cursor = context.getContentResolver().query(uri, new String[]{OpenableColumns.DISPLAY_NAME}, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
return cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
}
}
return result;
throw new FileNotFoundException("Could not resolve filename of " + uri);
}

@NonNull
Expand Down