Commit 0195b499 authored by Tobias Sargeant's avatar Tobias Sargeant Committed by Commit Bot

Unfork Minidump uploading code from weblayer.

This has the combined effect of breaking out the core uploading code in
MinidumpUploadCallable into MinidumpUploader, which can be used
separately. Management of file renaming still remains split over
multiple classes.

MinidumpUploadCallable tests are split to reflect the new code
organization.

Bug: 1029724
Test: run_chrome_public_test_apk \
Test: -f 'org.chromium.components.minidump_uploader.*#*'
Test: run_chrome_public_test_apk \
Test: -f 'org.chromium.chrome.browser.crash.*#*'
Test: run_webview_instrumentation_test_apk \
Test: -f 'org.chromium.android_webview.test.services.MinidumpUploadJobTest#*'
Change-Id: I0a8d7f8ad330d0127f7ce8559bd8090da6d44abe
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1975918Reviewed-by: default avatarIlya Sherman <isherman@chromium.org>
Reviewed-by: default avatarMugdha Lakhani <nator@chromium.org>
Commit-Queue: Tobias Sargeant <tobiasjs@chromium.org>
Cr-Commit-Position: refs/heads/master@{#733937}
parent 0fd69f5e
......@@ -38,6 +38,7 @@ android_library("minidump_uploader_java") {
"android/java/src/org/chromium/components/minidump_uploader/MinidumpUploadJob.java",
"android/java/src/org/chromium/components/minidump_uploader/MinidumpUploadJobImpl.java",
"android/java/src/org/chromium/components/minidump_uploader/MinidumpUploadJobService.java",
"android/java/src/org/chromium/components/minidump_uploader/MinidumpUploader.java",
"android/java/src/org/chromium/components/minidump_uploader/MinidumpUploaderDelegate.java",
"android/java/src/org/chromium/components/minidump_uploader/util/CrashReportingPermissionManager.java",
"android/java/src/org/chromium/components/minidump_uploader/util/HttpURLConnectionFactory.java",
......@@ -62,6 +63,7 @@ android_library("minidump_uploader_javatests") {
"android/javatests/src/org/chromium/components/minidump_uploader/MinidumpUploadCallableTest.java",
"android/javatests/src/org/chromium/components/minidump_uploader/MinidumpUploadJobImplTest.java",
"android/javatests/src/org/chromium/components/minidump_uploader/MinidumpUploadTestUtility.java",
"android/javatests/src/org/chromium/components/minidump_uploader/MinidumpUploaderTest.java",
"android/javatests/src/org/chromium/components/minidump_uploader/TestMinidumpUploadJobImpl.java",
"android/javatests/src/org/chromium/components/minidump_uploader/TestMinidumpUploaderDelegate.java",
]
......
......@@ -5,29 +5,17 @@
package org.chromium.components.minidump_uploader;
import androidx.annotation.IntDef;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.Log;
import org.chromium.base.StreamUtil;
import org.chromium.components.minidump_uploader.util.CrashReportingPermissionManager;
import org.chromium.components.minidump_uploader.util.HttpURLConnectionFactory;
import org.chromium.components.minidump_uploader.util.HttpURLConnectionFactoryImpl;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.net.HttpURLConnection;
import java.util.Locale;
import java.util.concurrent.Callable;
import java.util.zip.GZIPOutputStream;
/**
* This class tries to upload a minidump to the crash server.
......@@ -42,12 +30,6 @@ public class MinidumpUploadCallable implements Callable<Integer> {
// "crash_dump_week_upload_size" - Deprecated prefs used for limiting crash report uploads over
// cellular network. Last used in M47, removed in M78.
@VisibleForTesting
protected static final String CRASH_URL_STRING = "https://clients2.google.com/cr/report";
@VisibleForTesting
protected static final String CONTENT_TYPE_TMPL = "multipart/form-data; boundary=%s";
@IntDef({MinidumpUploadStatus.SUCCESS, MinidumpUploadStatus.FAILURE,
MinidumpUploadStatus.USER_DISABLED, MinidumpUploadStatus.DISABLED_BY_SAMPLING})
@Retention(RetentionPolicy.SOURCE)
......@@ -60,20 +42,19 @@ public class MinidumpUploadCallable implements Callable<Integer> {
private final File mFileToUpload;
private final File mLogfile;
private final HttpURLConnectionFactory mHttpURLConnectionFactory;
private final CrashReportingPermissionManager mPermManager;
private final MinidumpUploader mMinidumpUploader;
public MinidumpUploadCallable(
File fileToUpload, File logfile, CrashReportingPermissionManager permissionManager) {
this(fileToUpload, logfile, new HttpURLConnectionFactoryImpl(), permissionManager);
this(fileToUpload, logfile, new MinidumpUploader(), permissionManager);
}
public MinidumpUploadCallable(File fileToUpload, File logfile,
HttpURLConnectionFactory httpURLConnectionFactory,
CrashReportingPermissionManager permissionManager) {
MinidumpUploader minidumpUploader, CrashReportingPermissionManager permissionManager) {
mFileToUpload = fileToUpload;
mLogfile = logfile;
mHttpURLConnectionFactory = httpURLConnectionFactory;
mMinidumpUploader = minidumpUploader;
mPermManager = permissionManager;
}
......@@ -102,74 +83,9 @@ public class MinidumpUploadCallable implements Callable<Integer> {
}
}
HttpURLConnection connection =
mHttpURLConnectionFactory.createHttpURLConnection(CRASH_URL_STRING);
if (connection == null) {
return MinidumpUploadStatus.FAILURE;
}
FileInputStream minidumpInputStream = null;
try {
if (!configureConnectionForHttpPost(connection)) {
return MinidumpUploadStatus.FAILURE;
}
minidumpInputStream = new FileInputStream(mFileToUpload);
streamCopy(minidumpInputStream, new GZIPOutputStream(connection.getOutputStream()));
boolean success = handleExecutionResponse(connection);
return success ? MinidumpUploadStatus.SUCCESS : MinidumpUploadStatus.FAILURE;
} catch (IOException | ArrayIndexOutOfBoundsException e) {
// ArrayIndexOutOfBoundsException due to bad GZIPOutputStream implementation on some
// old sony devices.
// For now just log the stack trace.
Log.w(TAG, "Error while uploading " + mFileToUpload.getName(), e);
return MinidumpUploadStatus.FAILURE;
} finally {
connection.disconnect();
if (minidumpInputStream != null) {
StreamUtil.closeQuietly(minidumpInputStream);
}
}
}
/**
* Configures a HttpURLConnection to send a HTTP POST request for uploading the minidump.
*
* This also reads the content-type from the minidump file.
*
* @param connection the HttpURLConnection to configure
* @return true if successful.
* @throws IOException
*/
private boolean configureConnectionForHttpPost(HttpURLConnection connection)
throws IOException {
// Read the boundary which we need for the content type.
String boundary = readBoundary();
if (boundary == null) {
return false;
}
connection.setDoOutput(true);
connection.setRequestProperty("Connection", "Keep-Alive");
connection.setRequestProperty("Content-Encoding", "gzip");
connection.setRequestProperty("Content-Type", String.format(CONTENT_TYPE_TMPL, boundary));
return true;
}
/**
* Reads the HTTP response and cleans up successful uploads.
*
* @param connection the connection to read the response from
* @return true if the upload was successful, false otherwise.
* @throws IOException
*/
private Boolean handleExecutionResponse(HttpURLConnection connection) throws IOException {
int responseCode = connection.getResponseCode();
if (isSuccessful(responseCode)) {
String responseContent = getResponseContentAsString(connection);
// The crash server returns the crash ID.
String uploadId = responseContent != null ? responseContent : "unknown";
MinidumpUploader.Result result = mMinidumpUploader.upload(mFileToUpload);
if (result.isSuccess()) {
String uploadId = result.message();
String crashFileName = mFileToUpload.getName();
Log.i(TAG, "Minidump " + crashFileName + " uploaded successfully, id: " + uploadId);
......@@ -184,18 +100,25 @@ public class MinidumpUploadCallable implements Callable<Integer> {
} catch (IOException ioe) {
Log.e(TAG, "Fail to write uploaded entry to log file");
}
return true;
} else {
return MinidumpUploadStatus.SUCCESS;
}
if (result.isUploadError()) {
// Log the results of the upload. Note that periodic upload failures aren't bad
// because we will need to throttle uploads in the future anyway.
String msg = String.format(Locale.US, "Failed to upload %s with code: %d (%s).",
mFileToUpload.getName(), responseCode, connection.getResponseMessage());
mFileToUpload.getName(), result.errorCode(), result.message());
Log.i(TAG, msg);
// TODO(acleung): The return status informs us about why an upload might be
// rejected. The next logical step is to put the reasons in an UMA histogram.
return false;
} else {
Log.e(TAG,
"Local error while uploading " + mFileToUpload.getName() + ": "
+ result.message());
}
return MinidumpUploadStatus.FAILURE;
}
/**
......@@ -228,83 +151,4 @@ public class MinidumpUploadCallable implements Callable<Integer> {
}
}
/**
* Get the boundary from the file, we need it for the content-type.
*
* @return the boundary if found, else null.
* @throws IOException
*/
private String readBoundary() throws IOException {
BufferedReader reader = new BufferedReader(new FileReader(mFileToUpload));
String boundary = reader.readLine();
reader.close();
if (boundary == null || boundary.trim().isEmpty()) {
Log.e(TAG, "Ignoring invalid crash dump: '" + mFileToUpload + "'");
return null;
}
boundary = boundary.trim();
if (!boundary.startsWith("--") || boundary.length() < 10) {
Log.e(TAG, "Ignoring invalidly bound crash dump: '" + mFileToUpload + "'");
return null;
}
// Note: The regex allows all alphanumeric characters, as well as dashes.
// This matches the code that generates minidumps boundaries:
// https://chromium.googlesource.com/crashpad/crashpad/+/0c322ecc3f711c34fbf85b2cbe69f38b8dbccf05/util/net/http_multipart_builder.cc#36
if (!boundary.matches("^[a-zA-Z0-9-]*$")) {
Log.e(TAG,
"Ignoring invalidly bound crash dump '" + mFileToUpload
+ "' due to invalid boundary characters: '" + boundary + "'");
return null;
}
boundary = boundary.substring(2); // Remove the initial --
return boundary;
}
/**
* Returns whether the response code indicates a successful HTTP request.
*
* @param responseCode the response code
* @return true if response code indicates success, false otherwise.
*/
private static boolean isSuccessful(int responseCode) {
return responseCode == 200 || responseCode == 201 || responseCode == 202;
}
/**
* Reads the response from |connection| as a String.
*
* @param connection the connection to read the response from.
* @return the content of the response.
* @throws IOException
*/
private static String getResponseContentAsString(HttpURLConnection connection)
throws IOException {
String responseContent = null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
streamCopy(connection.getInputStream(), baos);
if (baos.size() > 0) {
responseContent = baos.toString();
}
return responseContent;
}
/**
* Copies all available data from |inStream| to |outStream|. Closes both
* streams when done.
*
* @param inStream the stream to read
* @param outStream the stream to write to
* @throws IOException
*/
private static void streamCopy(InputStream inStream, OutputStream outStream)
throws IOException {
byte[] temp = new byte[4096];
int bytesRead = inStream.read(temp);
while (bytesRead >= 0) {
outStream.write(temp, 0, bytesRead);
bytesRead = inStream.read(temp);
}
inStream.close();
outStream.close();
}
}
// Copyright 2019 The Chromium Authors. All rights reserved.
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.weblayer_private;
package org.chromium.components.minidump_uploader;
import org.chromium.components.minidump_uploader.util.HttpURLConnectionFactory;
import org.chromium.components.minidump_uploader.util.HttpURLConnectionFactoryImpl;
......@@ -25,33 +25,75 @@ import java.util.zip.GZIPOutputStream;
* boundary forms the first line of the file.
*/
public class MinidumpUploader {
// TODO(crbug.com/1029724) unfork this class back to //components/minidump_uploader
private static final String CRASH_URL_STRING = "https://clients2.google.com/cr/report";
private static final String CONTENT_TYPE_TMPL = "multipart/form-data; boundary=%s";
/* package */
static final String CRASH_URL_STRING = "https://clients2.google.com/cr/report";
/* package */
static final String CONTENT_TYPE_TMPL = "multipart/form-data; boundary=%s";
private final HttpURLConnectionFactory mHttpURLConnectionFactory;
/* package */ static final class Result {
final boolean mSuccess;
final int mStatus;
final String mResult;
/**
* The result of an upload attempt.
*
* An upload attempt may succeed, in which case the result message is the upload ID.
* Alternatively it may fail either as a result of either a local or a remote error.
*/
public static final class Result {
private final int mErrorCode;
private final String mResult;
private Result(boolean success, int status, String result) {
mSuccess = success;
mStatus = status;
private Result(int errorCode, String result) {
mErrorCode = errorCode;
mResult = result;
}
/** Returns true if this result represents a succesful upload. */
public boolean isSuccess() {
return mErrorCode == 0;
}
/** Returns true if this result represents a remote error. */
public boolean isUploadError() {
return mErrorCode > 0;
}
/** Returns true if this result represents a local error. */
public boolean isFailure() {
return mErrorCode < 0;
}
/**
* Returns the upload error code.
*
* @return 0 on success
* @return <0 on local error
* @return HTTP status code on remote error
*/
public int errorCode() {
return mErrorCode;
}
/**
* The message associated with this result.
*
* @return the remotely assigned upload id, on success
* @return descriptive error text otherwise.
*/
public String message() {
return mResult;
}
static Result failure(String result) {
return new Result(false, -1, result);
return new Result(-1, result);
}
static Result failure(int status, String result) {
return new Result(false, status, result);
static Result uploadError(int status, String result) {
assert status > 0;
return new Result(status, result);
}
static Result success(String result) {
return new Result(true, 0, result);
return new Result(0, result);
}
}
......@@ -63,6 +105,16 @@ public class MinidumpUploader {
mHttpURLConnectionFactory = httpURLConnectionFactory;
}
/**
* Attempt to upload a single file to the crash server.
*
* The result of the upload attempt is either success (and an associated report ID), or failure.
* Failure may occur locally (the file is invalid or the network connection could not be
* created) or remotely (the crash server rejected the upload with a HTTP error).
*
* @param fileToUpload the file containing a MIME-body with an attached minidump.
* @return the success/failure result of the upload attempt.
*/
public Result upload(File fileToUpload) {
try {
if (fileToUpload == null || !fileToUpload.exists()) {
......@@ -87,13 +139,13 @@ public class MinidumpUploader {
return Result.success(uploadId);
} else {
// Return the remote error code and message.
return Result.failure(responseCode, connection.getResponseMessage());
return Result.uploadError(responseCode, connection.getResponseMessage());
}
} finally {
connection.disconnect();
}
} catch (IOException | RuntimeException e) {
return Result.failure(e.getMessage());
return Result.failure(e.toString());
}
}
......@@ -113,28 +165,28 @@ public class MinidumpUploader {
}
/**
* Get the boundary from the file, we need it for the content-type.
* Get the MIME boundary from the file, for inclusion in Content-Type header.
*
* @return the boundary if found, else null.
* @throws IOException
* @return the MIME boundary used in the file.
* @throws IOException if fileToUpload cannot be read
* @throws RuntimeException if the MIME boundary is missing or malformed.
*/
private String readBoundary(File fileToUpload) throws IOException {
try (FileReader fileReader = new FileReader(fileToUpload);
BufferedReader reader = new BufferedReader(fileReader)) {
String boundary = reader.readLine();
if (boundary == null || boundary.trim().isEmpty()) {
throw new RuntimeException(fileToUpload + " does not have a MIME boundary");
throw new RuntimeException("File does not have a MIME boundary");
}
boundary = boundary.trim();
if (!boundary.startsWith("--") || boundary.length() < 10) {
throw new RuntimeException(fileToUpload + " does not have a MIME boundary");
throw new RuntimeException("File does not have a MIME boundary");
}
// Note: The regex allows all alphanumeric characters, as well as dashes.
// This matches the code that generates minidumps boundaries:
// https://chromium.googlesource.com/crashpad/crashpad/+/0c322ecc3f711c34fbf85b2cbe69f38b8dbccf05/util/net/http_multipart_builder.cc#36
if (!boundary.matches("^[a-zA-Z0-9-]*$")) {
throw new RuntimeException(
fileToUpload.getName() + " has an illegal MIME boundary: " + boundary);
throw new RuntimeException("File has an illegal MIME boundary: " + boundary);
}
boundary = boundary.substring(2); // Remove the initial --
return boundary;
......
......@@ -12,173 +12,61 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.test.BaseJUnit4ClassRunner;
import org.chromium.base.test.util.Feature;
import org.chromium.components.minidump_uploader.CrashTestRule.MockCrashReportingPermissionManager;
import org.chromium.components.minidump_uploader.MinidumpUploadCallable.MinidumpUploadStatus;
import org.chromium.components.minidump_uploader.util.CrashReportingPermissionManager;
import org.chromium.components.minidump_uploader.util.HttpURLConnectionFactory;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
/**
* Unittests for {@link MinidumpUploadCallable}.
*/
@RunWith(BaseJUnit4ClassRunner.class)
public class MinidumpUploadCallableTest {
private static final String LOCAL_CRASH_ID = "123_log";
private static final String LOG_FILE_NAME = "chromium_renderer-123_log.dmp224";
@Rule
public CrashTestRule mTestRule = new CrashTestRule();
private static final String BOUNDARY = "TESTBOUNDARY";
private static final String UPLOAD_CRASH_ID = "IMACRASHID";
private static final String LOCAL_CRASH_ID = "123_log";
private static final String LOG_FILE_NAME = "chromium_renderer-123_log.dmp224";
private File mTestUpload;
private File mUploadLog;
private File mExpectedFileAfterUpload;
/**
* A HttpURLConnection that performs some basic checks to ensure we are uploading
* minidumps correctly.
*/
public static class TestHttpURLConnection extends HttpURLConnection {
static final String DEFAULT_EXPECTED_CONTENT_TYPE =
String.format(MinidumpUploadCallable.CONTENT_TYPE_TMPL, BOUNDARY);
private final String mExpectedContentType;
/**
* The value of the "Content-Type" property if the property has been set.
*/
private String mContentTypePropertyValue = "";
public TestHttpURLConnection(URL url) {
this(url, DEFAULT_EXPECTED_CONTENT_TYPE);
}
public TestHttpURLConnection(URL url, String contentType) {
super(url);
mExpectedContentType = contentType;
Assert.assertEquals(MinidumpUploadCallable.CRASH_URL_STRING, url.toString());
}
@Override
public void disconnect() {
// Check that the "Content-Type" property has been set and the property's value.
Assert.assertEquals(mExpectedContentType, mContentTypePropertyValue);
}
@Override
public InputStream getInputStream() {
return new ByteArrayInputStream(ApiCompatibilityUtils.getBytesUtf8(UPLOAD_CRASH_ID));
}
@Override
public OutputStream getOutputStream() {
return new ByteArrayOutputStream();
}
@Override
public int getResponseCode() {
return 200;
}
@Override
public String getResponseMessage() {
return null;
}
@Override
public boolean usingProxy() {
return false;
}
@Override
public void connect() {
}
@Override
public void setRequestProperty(String key, String value) {
if (key.equals("Content-Type")) {
mContentTypePropertyValue = value;
}
}
}
/**
* A HttpURLConnectionFactory that performs some basic checks to ensure we are uploading
* minidumps correctly.
*/
public static class TestHttpURLConnectionFactory implements HttpURLConnectionFactory {
String mContentType;
private static class MockMinidumpUploader extends MinidumpUploader {
private Result mMockResult;
public TestHttpURLConnectionFactory() {
mContentType = TestHttpURLConnection.DEFAULT_EXPECTED_CONTENT_TYPE;
public static MinidumpUploader returnsSuccess() {
return new MockMinidumpUploader(Result.success(MinidumpUploaderTest.UPLOAD_CRASH_ID));
}
@Override
public HttpURLConnection createHttpURLConnection(String url) {
try {
return new TestHttpURLConnection(new URL(url), mContentType);
} catch (IOException e) {
return null;
}
public static MinidumpUploader returnsFailure(String message) {
return new MockMinidumpUploader(Result.failure(message));
}
}
private static class ErrorCodeHttpUrlConnectionFactory implements HttpURLConnectionFactory {
private final int mErrorCode;
ErrorCodeHttpUrlConnectionFactory(int errorCode) {
mErrorCode = errorCode;
public static MinidumpUploader returnsUploadError(int status, String message) {
return new MockMinidumpUploader(Result.uploadError(status, message));
}
@Override
public HttpURLConnection createHttpURLConnection(String url) {
try {
return new TestHttpURLConnection(new URL(url)) {
@Override
public int getResponseCode() {
return mErrorCode;
}
};
} catch (IOException e) {
return null;
}
}
private MockMinidumpUploader(Result mockResult) {
super(null);
mMockResult = mockResult;
}
private static class FailHttpURLConnectionFactory implements HttpURLConnectionFactory {
@Override
public HttpURLConnection createHttpURLConnection(String url) {
Assert.fail();
return null;
}
}
/**
* This class calls |getInstrumentation| which cannot be done in a static context.
*/
private class MockMinidumpUploadCallable extends MinidumpUploadCallable {
MockMinidumpUploadCallable(
HttpURLConnectionFactory httpURLConnectionFactory,
CrashReportingPermissionManager permManager) {
super(mTestUpload, mUploadLog, httpURLConnectionFactory, permManager);
public Result upload(File fileToUpload) {
return mMockResult;
}
}
private void createMinidumpFile() throws Exception {
mTestUpload = new File(mTestRule.getCrashDir(), LOG_FILE_NAME);
CrashTestRule.setUpMinidumpFile(mTestUpload, BOUNDARY);
CrashTestRule.setUpMinidumpFile(mTestUpload, MinidumpUploaderTest.BOUNDARY);
}
private void setForcedUpload() {
......@@ -201,6 +89,50 @@ public class MinidumpUploadCallableTest {
new File(mTestRule.getCrashDir(), mTestUpload.getName().replace(".dmp", ".up"));
}
@Test
@SmallTest
@Feature({"Android-AppBase"})
public void testSuccessfulUpload() throws Exception {
final CrashReportingPermissionManager testPermManager =
new MockCrashReportingPermissionManager() {
{ mIsEnabledForTests = true; }
};
MinidumpUploadCallable minidumpUploadCallable = new MinidumpUploadCallable(
mTestUpload, mUploadLog, MockMinidumpUploader.returnsSuccess(), testPermManager);
Assert.assertEquals(MinidumpUploadStatus.SUCCESS, minidumpUploadCallable.call().intValue());
Assert.assertTrue(mExpectedFileAfterUpload.exists());
assertValidUploadLogEntry();
}
@Test
@SmallTest
@Feature({"Android-AppBase"})
public void testFailedUploadLocalError() throws Exception {
final CrashReportingPermissionManager testPermManager =
new MockCrashReportingPermissionManager() {
{ mIsEnabledForTests = true; }
};
MinidumpUploadCallable minidumpUploadCallable = new MinidumpUploadCallable(mTestUpload,
mUploadLog, MockMinidumpUploader.returnsFailure("Failed"), testPermManager);
Assert.assertEquals(MinidumpUploadStatus.FAILURE, minidumpUploadCallable.call().intValue());
Assert.assertFalse(mExpectedFileAfterUpload.exists());
}
@Test
@SmallTest
@Feature({"Android-AppBase"})
public void testFailedUploadRemoteError() throws Exception {
final CrashReportingPermissionManager testPermManager =
new MockCrashReportingPermissionManager() {
{ mIsEnabledForTests = true; }
};
MinidumpUploadCallable minidumpUploadCallable =
new MinidumpUploadCallable(mTestUpload, mUploadLog,
MockMinidumpUploader.returnsUploadError(404, "Not Found"), testPermManager);
Assert.assertEquals(MinidumpUploadStatus.FAILURE, minidumpUploadCallable.call().intValue());
Assert.assertFalse(mExpectedFileAfterUpload.exists());
}
@Test
@SmallTest
@Feature({"Android-AppBase"})
......@@ -215,10 +147,8 @@ public class MinidumpUploadCallableTest {
}
};
HttpURLConnectionFactory httpURLConnectionFactory = new TestHttpURLConnectionFactory();
MinidumpUploadCallable minidumpUploadCallable =
new MockMinidumpUploadCallable(httpURLConnectionFactory, testPermManager);
MinidumpUploadCallable minidumpUploadCallable = new MinidumpUploadCallable(
mTestUpload, mUploadLog, MockMinidumpUploader.returnsSuccess(), testPermManager);
Assert.assertEquals(MinidumpUploadStatus.SUCCESS, minidumpUploadCallable.call().intValue());
Assert.assertTrue(mExpectedFileAfterUpload.exists());
assertValidUploadLogEntry();
......@@ -238,10 +168,8 @@ public class MinidumpUploadCallableTest {
}
};
HttpURLConnectionFactory httpURLConnectionFactory = new FailHttpURLConnectionFactory();
MinidumpUploadCallable minidumpUploadCallable =
new MockMinidumpUploadCallable(httpURLConnectionFactory, testPermManager);
MinidumpUploadCallable minidumpUploadCallable = new MinidumpUploadCallable(
mTestUpload, mUploadLog, MockMinidumpUploader.returnsSuccess(), testPermManager);
Assert.assertEquals(
MinidumpUploadStatus.USER_DISABLED, minidumpUploadCallable.call().intValue());
......@@ -265,10 +193,8 @@ public class MinidumpUploadCallableTest {
}
};
HttpURLConnectionFactory httpURLConnectionFactory = new TestHttpURLConnectionFactory();
MinidumpUploadCallable minidumpUploadCallable =
new MockMinidumpUploadCallable(httpURLConnectionFactory, testPermManager);
MinidumpUploadCallable minidumpUploadCallable = new MinidumpUploadCallable(
mTestUpload, mUploadLog, MockMinidumpUploader.returnsSuccess(), testPermManager);
Assert.assertEquals(MinidumpUploadStatus.DISABLED_BY_SAMPLING,
minidumpUploadCallable.call().intValue());
......@@ -292,10 +218,8 @@ public class MinidumpUploadCallableTest {
}
};
HttpURLConnectionFactory httpURLConnectionFactory = new FailHttpURLConnectionFactory();
MinidumpUploadCallable minidumpUploadCallable =
new MockMinidumpUploadCallable(httpURLConnectionFactory, testPermManager);
MinidumpUploadCallable minidumpUploadCallable = new MinidumpUploadCallable(
mTestUpload, mUploadLog, MockMinidumpUploader.returnsSuccess(), testPermManager);
Assert.assertEquals(MinidumpUploadStatus.FAILURE, minidumpUploadCallable.call().intValue());
Assert.assertFalse(mExpectedFileAfterUpload.exists());
}
......@@ -314,10 +238,8 @@ public class MinidumpUploadCallableTest {
}
};
HttpURLConnectionFactory httpURLConnectionFactory = new TestHttpURLConnectionFactory();
MinidumpUploadCallable minidumpUploadCallable =
new MockMinidumpUploadCallable(httpURLConnectionFactory, testPermManager);
MinidumpUploadCallable minidumpUploadCallable = new MinidumpUploadCallable(
mTestUpload, mUploadLog, MockMinidumpUploader.returnsSuccess(), testPermManager);
Assert.assertEquals(MinidumpUploadStatus.SUCCESS, minidumpUploadCallable.call().intValue());
Assert.assertTrue(mExpectedFileAfterUpload.exists());
assertValidUploadLogEntry();
......@@ -338,10 +260,8 @@ public class MinidumpUploadCallableTest {
}
};
HttpURLConnectionFactory httpURLConnectionFactory = new TestHttpURLConnectionFactory();
MinidumpUploadCallable minidumpUploadCallable =
new MockMinidumpUploadCallable(httpURLConnectionFactory, testPermManager);
MinidumpUploadCallable minidumpUploadCallable = new MinidumpUploadCallable(
mTestUpload, mUploadLog, MockMinidumpUploader.returnsSuccess(), testPermManager);
Assert.assertEquals(MinidumpUploadStatus.SUCCESS, minidumpUploadCallable.call().intValue());
Assert.assertTrue(mExpectedFileAfterUpload.exists());
assertValidUploadLogEntry();
......@@ -362,10 +282,8 @@ public class MinidumpUploadCallableTest {
}
};
HttpURLConnectionFactory httpURLConnectionFactory = new TestHttpURLConnectionFactory();
MinidumpUploadCallable minidumpUploadCallable =
new MockMinidumpUploadCallable(httpURLConnectionFactory, testPermManager);
MinidumpUploadCallable minidumpUploadCallable = new MinidumpUploadCallable(
mTestUpload, mUploadLog, MockMinidumpUploader.returnsSuccess(), testPermManager);
Assert.assertEquals(MinidumpUploadStatus.SUCCESS, minidumpUploadCallable.call().intValue());
File expectedSkippedFileAfterUpload = new File(
......@@ -389,10 +307,8 @@ public class MinidumpUploadCallableTest {
}
};
HttpURLConnectionFactory httpURLConnectionFactory = new TestHttpURLConnectionFactory();
MinidumpUploadCallable minidumpUploadCallable =
new MockMinidumpUploadCallable(httpURLConnectionFactory, testPermManager);
MinidumpUploadCallable minidumpUploadCallable = new MinidumpUploadCallable(
mTestUpload, mUploadLog, MockMinidumpUploader.returnsSuccess(), testPermManager);
Assert.assertEquals(MinidumpUploadStatus.SUCCESS, minidumpUploadCallable.call().intValue());
File expectedSkippedFileAfterUpload = new File(
......@@ -416,10 +332,8 @@ public class MinidumpUploadCallableTest {
}
};
HttpURLConnectionFactory httpURLConnectionFactory = new TestHttpURLConnectionFactory();
MinidumpUploadCallable minidumpUploadCallable =
new MockMinidumpUploadCallable(httpURLConnectionFactory, testPermManager);
MinidumpUploadCallable minidumpUploadCallable = new MinidumpUploadCallable(
mTestUpload, mUploadLog, MockMinidumpUploader.returnsSuccess(), testPermManager);
Assert.assertEquals(MinidumpUploadStatus.SUCCESS, minidumpUploadCallable.call().intValue());
File expectedSkippedFileAfterUpload = new File(
......@@ -428,82 +342,6 @@ public class MinidumpUploadCallableTest {
Assert.assertTrue(mExpectedFileAfterUpload.exists());
}
// This is a regression test for http://crbug.com/712420
@Test
@SmallTest
@Feature({"Android-AppBase"})
public void testCallWithInvalidMinidumpBoundary() throws Exception {
// Include an invalid character, '[', in the test string.
CrashTestRule.setUpMinidumpFile(mTestUpload, "--InvalidBoundaryWithSpecialCharacter--[");
CrashReportingPermissionManager testPermManager =
new MockCrashReportingPermissionManager() {
{ mIsEnabledForTests = true; }
};
HttpURLConnectionFactory httpURLConnectionFactory = new TestHttpURLConnectionFactory() {
{ mContentType = ""; }
};
MinidumpUploadCallable minidumpUploadCallable =
new MockMinidumpUploadCallable(httpURLConnectionFactory, testPermManager);
Assert.assertEquals(MinidumpUploadStatus.FAILURE, minidumpUploadCallable.call().intValue());
Assert.assertFalse(mExpectedFileAfterUpload.exists());
}
@Test
@SmallTest
@Feature({"Android-AppBase"})
public void testCallWithValidMinidumpBoundary() throws Exception {
// Include all valid characters in the test string.
final String boundary = "--0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
final String expectedContentType =
String.format(MinidumpUploadCallable.CONTENT_TYPE_TMPL, boundary);
CrashReportingPermissionManager testPermManager =
new MockCrashReportingPermissionManager() {
{ mIsEnabledForTests = true; }
};
HttpURLConnectionFactory httpURLConnectionFactory = new TestHttpURLConnectionFactory() {
{ mContentType = expectedContentType; }
};
CrashTestRule.setUpMinidumpFile(mTestUpload, boundary);
MinidumpUploadCallable minidumpUploadCallable =
new MockMinidumpUploadCallable(httpURLConnectionFactory, testPermManager);
Assert.assertEquals(MinidumpUploadStatus.SUCCESS, minidumpUploadCallable.call().intValue());
Assert.assertTrue(mExpectedFileAfterUpload.exists());
assertValidUploadLogEntry();
}
@Test
@SmallTest
@Feature({"Android-AppBase"})
public void testReceivingErrorCodes() {
CrashReportingPermissionManager testPermManager =
new MockCrashReportingPermissionManager() {
{
mIsInSample = true;
mIsUserPermitted = true;
mIsNetworkAvailable = true;
mIsEnabledForTests = false;
}
};
final int[] errorCodes = {400, 401, 403, 404, 500};
for (int n = 0; n < errorCodes.length; n++) {
HttpURLConnectionFactory httpURLConnectionFactory =
new ErrorCodeHttpUrlConnectionFactory(errorCodes[n]);
MinidumpUploadCallable minidumpUploadCallable =
new MockMinidumpUploadCallable(httpURLConnectionFactory, testPermManager);
Assert.assertEquals(
MinidumpUploadStatus.FAILURE, minidumpUploadCallable.call().intValue());
// Note that mTestUpload is not renamed on failure - so we can try to upload that file
// several times during the same test.
}
}
private void assertValidUploadLogEntry() throws IOException {
File logfile = new File(mTestRule.getCrashDir(), CrashFileManager.CRASH_DUMP_LOGFILE);
BufferedReader input = new BufferedReader(new FileReader(logfile));
......@@ -532,7 +370,7 @@ public class MinidumpUploadCallableTest {
Assert.assertTrue(time <= now);
Assert.assertTrue(time > now - 60 * 60);
Assert.assertEquals(uploadId, UPLOAD_CRASH_ID);
Assert.assertEquals(uploadId, MinidumpUploaderTest.UPLOAD_CRASH_ID);
Assert.assertEquals(localId, LOCAL_CRASH_ID);
}
}
......@@ -14,6 +14,8 @@ import org.junit.runner.RunWith;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.BaseJUnit4ClassRunner;
import org.chromium.components.minidump_uploader.CrashTestRule.MockCrashReportingPermissionManager;
import org.chromium.components.minidump_uploader.MinidumpUploaderTest.TestHttpURLConnection;
import org.chromium.components.minidump_uploader.MinidumpUploaderTest.TestHttpURLConnectionFactory;
import org.chromium.components.minidump_uploader.util.CrashReportingPermissionManager;
import org.chromium.components.minidump_uploader.util.HttpURLConnectionFactory;
......@@ -39,6 +41,31 @@ public class MinidumpUploadJobImplTest {
private static final String BOUNDARY = "TESTBOUNDARY";
/**
* Test to ensure the minidump uploading mechanism allows the expected number of upload retries.
*/
@Test
@MediumTest
public void testRetryCountRespected() throws IOException {
final CrashReportingPermissionManager permManager =
new MockCrashReportingPermissionManager() {
{
mIsInSample = true;
mIsUserPermitted = true;
mIsNetworkAvailable = false; // Will cause us to fail uploads
mIsEnabledForTests = false;
}
};
File firstFile = createMinidumpFileInCrashDir("1_abc.dmp0.try0");
for (int i = 0; i < MinidumpUploadJobImpl.MAX_UPLOAD_TRIES_ALLOWED; ++i) {
MinidumpUploadTestUtility.uploadMinidumpsSync(
new TestMinidumpUploadJobImpl(mTestRule.getExistingCacheDir(), permManager),
i + 1 < MinidumpUploadJobImpl.MAX_UPLOAD_TRIES_ALLOWED);
}
}
/**
* Test to ensure the minidump uploading mechanism behaves as expected when we fail to upload
* minidumps.
......@@ -94,15 +121,15 @@ public class MinidumpUploadJobImplTest {
callables.add(new MinidumpUploadCallableCreator() {
@Override
public MinidumpUploadCallable createCallable(File minidumpFile, File logfile) {
return new MinidumpUploadCallable(
minidumpFile, logfile, new FailingHttpUrlConnectionFactory(), permManager);
return new MinidumpUploadCallable(minidumpFile, logfile,
new MinidumpUploader(new FailingHttpUrlConnectionFactory()), permManager);
}
});
callables.add(new MinidumpUploadCallableCreator() {
@Override
public MinidumpUploadCallable createCallable(File minidumpFile, File logfile) {
return new MinidumpUploadCallable(minidumpFile, logfile,
new MinidumpUploadCallableTest.TestHttpURLConnectionFactory(), permManager);
new MinidumpUploader(new TestHttpURLConnectionFactory()), permManager);
}
});
MinidumpUploadJob minidumpUploadJob = createCallableListMinidumpUploadJob(
......@@ -328,7 +355,8 @@ public class MinidumpUploadJobImplTest {
public MinidumpUploadCallable createMinidumpUploadCallable(
File minidumpFile, File logfile) {
return new MinidumpUploadCallable(minidumpFile, logfile,
new StallingHttpUrlConnectionFactory(mStopStallingLatch, mSuccessfulUpload),
new MinidumpUploader(new StallingHttpUrlConnectionFactory(
mStopStallingLatch, mSuccessfulUpload)),
mDelegate.createCrashReportingPermissionManager());
}
}
......@@ -359,7 +387,7 @@ public class MinidumpUploadJobImplTest {
@Override
public HttpURLConnection createHttpURLConnection(String url) {
try {
return new MinidumpUploadCallableTest.TestHttpURLConnection(new URL(url)) {
return new TestHttpURLConnection(new URL(url)) {
@Override
public OutputStream getOutputStream() {
return new StallingOutputStream();
......
......@@ -13,6 +13,7 @@ import org.chromium.base.ThreadUtils;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Utility class for testing the minidump-uploading mechanism.
......@@ -69,17 +70,19 @@ public class MinidumpUploadTestUtility {
public static void uploadMinidumpsSync(
MinidumpUploadJob minidumpUploadJob, final boolean expectReschedule) {
final CountDownLatch uploadsFinishedLatch = new CountDownLatch(1);
AtomicBoolean wasRescheduled = new AtomicBoolean();
uploadAllMinidumpsOnUiThread(
minidumpUploadJob, new MinidumpUploadJob.UploadsFinishedCallback() {
@Override
public void uploadsFinished(boolean reschedule) {
assertEquals(expectReschedule, reschedule);
wasRescheduled.set(reschedule);
uploadsFinishedLatch.countDown();
}
});
try {
assertTrue(uploadsFinishedLatch.await(
scaleTimeout(TIME_OUT_MILLIS), TimeUnit.MILLISECONDS));
assertEquals(expectReschedule, wasRescheduled.get());
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
......
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.minidump_uploader;
import android.support.test.filters.SmallTest;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.test.BaseJUnit4ClassRunner;
import org.chromium.base.test.util.Feature;
import org.chromium.components.minidump_uploader.util.HttpURLConnectionFactory;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
/**
* Unittests for {@link MinidumpUploadCallable}.
*/
@RunWith(BaseJUnit4ClassRunner.class)
public class MinidumpUploaderTest {
/* package */ static final String BOUNDARY = "TESTBOUNDARY";
/* package */ static final String UPLOAD_CRASH_ID = "IMACRASHID";
@Rule
public CrashTestRule mTestRule = new CrashTestRule();
private File mUploadTestFile;
/**
* A HttpURLConnection that performs some basic checks to ensure we are uploading
* minidumps correctly.
*/
/* package */ static class TestHttpURLConnection extends HttpURLConnection {
static final String DEFAULT_EXPECTED_CONTENT_TYPE =
String.format(MinidumpUploader.CONTENT_TYPE_TMPL, BOUNDARY);
private final String mExpectedContentType;
/**
* The value of the "Content-Type" property if the property has been set.
*/
private String mContentTypePropertyValue = "";
public TestHttpURLConnection(URL url) {
this(url, DEFAULT_EXPECTED_CONTENT_TYPE);
}
public TestHttpURLConnection(URL url, String contentType) {
super(url);
mExpectedContentType = contentType;
Assert.assertEquals(MinidumpUploader.CRASH_URL_STRING, url.toString());
}
@Override
public void disconnect() {
// Check that the "Content-Type" property has been set and the property's value.
Assert.assertEquals(mExpectedContentType, mContentTypePropertyValue);
}
@Override
public InputStream getInputStream() {
return new ByteArrayInputStream(ApiCompatibilityUtils.getBytesUtf8(UPLOAD_CRASH_ID));
}
@Override
public OutputStream getOutputStream() {
return new ByteArrayOutputStream();
}
@Override
public int getResponseCode() {
return 200;
}
@Override
public String getResponseMessage() {
return null;
}
@Override
public boolean usingProxy() {
return false;
}
@Override
public void connect() {}
@Override
public void setRequestProperty(String key, String value) {
if (key.equals("Content-Type")) {
mContentTypePropertyValue = value;
}
}
}
/**
* A HttpURLConnectionFactory that performs some basic checks to ensure we are uploading
* minidumps correctly.
*/
/* package */ static class TestHttpURLConnectionFactory implements HttpURLConnectionFactory {
String mContentType;
public TestHttpURLConnectionFactory() {
mContentType = TestHttpURLConnection.DEFAULT_EXPECTED_CONTENT_TYPE;
}
@Override
public HttpURLConnection createHttpURLConnection(String url) {
try {
return new TestHttpURLConnection(new URL(url), mContentType);
} catch (IOException e) {
return null;
}
}
}
/* package */ static class ErrorCodeHttpURLConnectionFactory
implements HttpURLConnectionFactory {
private final int mErrorCode;
ErrorCodeHttpURLConnectionFactory(int errorCode) {
mErrorCode = errorCode;
}
@Override
public HttpURLConnection createHttpURLConnection(String url) {
try {
return new TestHttpURLConnection(new URL(url)) {
@Override
public int getResponseCode() {
return mErrorCode;
}
};
} catch (IOException e) {
return null;
}
}
}
/* package */ static class FailHttpURLConnectionFactory implements HttpURLConnectionFactory {
@Override
public HttpURLConnection createHttpURLConnection(String url) {
Assert.fail();
return null;
}
}
@Before
public void setUp() throws IOException {
mUploadTestFile = new File(mTestRule.getCrashDir(), "crashFile");
CrashTestRule.setUpMinidumpFile(mUploadTestFile, MinidumpUploaderTest.BOUNDARY);
}
@After
public void tearDown() throws IOException {
mUploadTestFile.delete();
}
// This is a regression test for http://crbug.com/712420
@Test
@SmallTest
@Feature({"Android-AppBase"})
public void testCallWithInvalidMinidumpBoundary() throws Exception {
// Include an invalid character, '[', in the test string.
final String boundary = "--InvalidBoundaryWithSpecialCharacter--[";
CrashTestRule.setUpMinidumpFile(mUploadTestFile, boundary);
HttpURLConnectionFactory httpURLConnectionFactory = new TestHttpURLConnectionFactory() {
{ mContentType = ""; }
};
MinidumpUploader minidumpUploader = new MinidumpUploader(httpURLConnectionFactory);
MinidumpUploader.Result result = minidumpUploader.upload(mUploadTestFile);
Assert.assertTrue(result.isFailure());
}
@Test
@SmallTest
@Feature({"Android-AppBase"})
public void testCallWithValidMinidumpBoundary() throws Exception {
// Include all valid characters in the test string.
final String boundary = "--0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
final String expectedContentType =
String.format(MinidumpUploader.CONTENT_TYPE_TMPL, boundary);
CrashTestRule.setUpMinidumpFile(mUploadTestFile, boundary);
HttpURLConnectionFactory httpURLConnectionFactory = new TestHttpURLConnectionFactory() {
{ mContentType = expectedContentType; }
};
MinidumpUploader minidumpUploader = new MinidumpUploader(httpURLConnectionFactory);
MinidumpUploader.Result result = minidumpUploader.upload(mUploadTestFile);
Assert.assertTrue(result.isSuccess());
}
@Test
@SmallTest
@Feature({"Android-AppBase"})
public void testReceivingErrorCodes() {
final int[] errorCodes = {400, 401, 403, 404, 500};
for (int n = 0; n < errorCodes.length; n++) {
HttpURLConnectionFactory httpURLConnectionFactory =
new ErrorCodeHttpURLConnectionFactory(errorCodes[n]);
MinidumpUploader minidumpUploader = new MinidumpUploader(httpURLConnectionFactory);
MinidumpUploader.Result result = minidumpUploader.upload(mUploadTestFile);
Assert.assertTrue(result.isUploadError());
Assert.assertEquals(result.errorCode(), errorCodes[n]);
}
}
}
......@@ -4,6 +4,7 @@
package org.chromium.components.minidump_uploader;
import org.chromium.components.minidump_uploader.MinidumpUploaderTest.TestHttpURLConnectionFactory;
import org.chromium.components.minidump_uploader.util.CrashReportingPermissionManager;
import java.io.File;
......@@ -32,7 +33,7 @@ public class TestMinidumpUploadJobImpl extends MinidumpUploadJobImpl {
@Override
public MinidumpUploadCallable createMinidumpUploadCallable(File minidumpFile, File logfile) {
return new MinidumpUploadCallable(minidumpFile, logfile,
new MinidumpUploadCallableTest.TestHttpURLConnectionFactory(),
new MinidumpUploader(new TestHttpURLConnectionFactory()),
mDelegate.createCrashReportingPermissionManager());
}
}
......@@ -42,7 +42,6 @@ android_library("java") {
"org/chromium/weblayer_private/FragmentWindowAndroid.java",
"org/chromium/weblayer_private/FullscreenCallbackProxy.java",
"org/chromium/weblayer_private/LocaleChangedBroadcastReceiver.java",
"org/chromium/weblayer_private/MinidumpUploader.java",
"org/chromium/weblayer_private/NavigationControllerImpl.java",
"org/chromium/weblayer_private/NavigationImpl.java",
"org/chromium/weblayer_private/NewTabCallbackProxy.java",
......
......@@ -18,6 +18,7 @@ import org.chromium.base.PathUtils;
import org.chromium.base.task.AsyncTask;
import org.chromium.components.crash.browser.ChildProcessCrashObserver;
import org.chromium.components.minidump_uploader.CrashFileManager;
import org.chromium.components.minidump_uploader.MinidumpUploader;
import org.chromium.weblayer_private.interfaces.ICrashReporterController;
import org.chromium.weblayer_private.interfaces.ICrashReporterControllerClient;
import org.chromium.weblayer_private.interfaces.StrictModeWorkaround;
......@@ -80,16 +81,16 @@ public final class CrashReporterControllerImpl extends ICrashReporterController.
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
File minidumpFile = getCrashFileManager().getCrashFileWithLocalId(localId);
MinidumpUploader.Result result = new MinidumpUploader().upload(minidumpFile);
if (result.mSuccess) {
if (result.isSuccess()) {
CrashFileManager.markUploadSuccess(minidumpFile);
} else {
CrashFileManager.tryIncrementAttemptNumber(minidumpFile);
}
try {
if (result.mSuccess) {
mClient.onCrashUploadSucceeded(localId, result.mResult);
if (result.isSuccess()) {
mClient.onCrashUploadSucceeded(localId, result.message());
} else {
mClient.onCrashUploadFailed(localId, result.mResult);
mClient.onCrashUploadFailed(localId, result.message());
}
} catch (RemoteException e) {
throw new AndroidRuntimeException(e);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment