Skip to content

Add uploadScreenshot to FirebaseAppDistributionTesterApiClient #3814

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

Merged
merged 5 commits into from
Jun 15, 2022
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
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@

package com.google.firebase.appdistribution.impl;

import static android.graphics.Bitmap.CompressFormat.PNG;
import static com.google.firebase.appdistribution.impl.TaskUtils.runAsyncInTask;

import android.graphics.Bitmap;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.Tasks;
import com.google.firebase.FirebaseApp;
Expand Down Expand Up @@ -58,30 +59,17 @@ private interface FidDependentJob<TResult> {
private static final String FIND_RELEASE_TAG = "Finding installed release";
private static final String CREATE_FEEDBACK_TAG = "Creating feedback";
private static final String COMMIT_FEEDBACK_TAG = "Committing feedback";
private static final String UPLOAD_SCREENSHOT_TAG = "Uploading screenshot";

private final FirebaseApp firebaseApp;
private final Provider<FirebaseInstallationsApi> firebaseInstallationsApiProvider;
private final TesterApiHttpClient testerApiHttpClient;
private final Executor taskExecutor;
private final Executor taskExecutor = Executors.newSingleThreadExecutor();

FirebaseAppDistributionTesterApiClient(
@NonNull FirebaseApp firebaseApp,
@NonNull Provider<FirebaseInstallationsApi> firebaseInstallationsApiProvider,
@NonNull TesterApiHttpClient testerApiHttpClient) {
this(
Executors.newSingleThreadExecutor(),
firebaseApp,
firebaseInstallationsApiProvider,
testerApiHttpClient);
}

@VisibleForTesting
FirebaseAppDistributionTesterApiClient(
@NonNull Executor taskExecutor,
@NonNull FirebaseApp firebaseApp,
@NonNull Provider<FirebaseInstallationsApi> firebaseInstallationsApiProvider,
@NonNull TesterApiHttpClient testerApiHttpClient) {
this.taskExecutor = taskExecutor;
this.firebaseApp = firebaseApp;
this.firebaseInstallationsApiProvider = firebaseInstallationsApiProvider;
this.testerApiHttpClient = testerApiHttpClient;
Expand Down Expand Up @@ -157,6 +145,27 @@ Task<String> createFeedback(String testerReleaseName, String feedbackText) {
});
}

/**
* Attaches a screenshot to feedback.
*
* @return a {@link Task} containing the feedback name, for convenience when chaining subsequent
* requests off of this task
*/
Task<String> attachScreenshot(String feedbackName, Bitmap screenshot) {
return runWithFidAndToken(
(unused, token) -> {
LogWrapper.getInstance().i("Uploading screenshot for feedback: " + feedbackName);
String path =
String.format("upload/v1alpha/%s:uploadArtifact?type=SCREENSHOT", feedbackName);
testerApiHttpClient.makeUploadRequest(
UPLOAD_SCREENSHOT_TAG,
path,
token,
stream -> screenshot.compress(PNG, /* quality= */ 100, stream));
return feedbackName;
});
}

/** Commits the feedback with the given name. */
@NonNull
Task<Void> commitFeedback(String feedbackName) {
Expand Down Expand Up @@ -258,8 +267,9 @@ private <TResult> Task<TResult> runWithFidAndToken(FidDependentJob<TResult> job)
firebaseInstallationsApiProvider.get().getToken(/* forceRefresh= */ false);

return Tasks.whenAllSuccess(installationIdTask, installationAuthTokenTask)
.continueWithTask(TaskUtils::handleTaskFailure)
.continueWithTask(taskExecutor, TaskUtils::handleTaskFailure)
.onSuccessTask(
taskExecutor,
resultsList -> {
// Tasks.whenAllSuccess combines task results into a list
if (resultsList.size() != 2) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,24 +27,27 @@
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.GZIPOutputStream;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see you now never compress and data. I think that's probably OK, since all our API calls (except the screenshot upload which does it's own compression) tend to have pretty small payloads.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I figure this seems OK for now and we can always revisit.

import javax.net.ssl.HttpsURLConnection;
import org.json.JSONException;
import org.json.JSONObject;

/** Client that makes FIS-authenticated GET and POST requests to the App Distribution Tester API. */
class TesterApiHttpClient {

/** Functional interface for a function that writes a request body to an output stream */
interface RequestBodyWriter {
void write(OutputStream os) throws IOException;
}

private static final String APP_TESTERS_HOST = "firebaseapptesters.googleapis.com";
private static final String REQUEST_METHOD_GET = "GET";
private static final String REQUEST_METHOD_POST = "POST";
private static final String CONTENT_TYPE_HEADER_KEY = "Content-Type";
private static final String JSON_CONTENT_TYPE = "application/json";
private static final String CONTENT_ENCODING_HEADER_KEY = "Content-Encoding";
private static final String GZIP_CONTENT_ENCODING = "gzip";
private static final String API_KEY_HEADER = "x-goog-api-key";
private static final String INSTALLATION_AUTH_HEADER = "X-Goog-Firebase-Installations-Auth";
private static final String X_ANDROID_PACKAGE_HEADER_KEY = "X-Android-Package";
Expand Down Expand Up @@ -110,7 +113,8 @@ JSONObject makePostRequest(String tag, String path, String token, String request
throw new FirebaseAppDistributionException(
"Unsupported encoding: " + UTF_8, Status.UNKNOWN, e);
}
return makePostRequest(tag, path, token, bytes, new HashMap<>());
return makePostRequest(
tag, path, token, new HashMap<>(), outputStream -> outputStream.write(bytes));
}

/**
Expand All @@ -120,34 +124,38 @@ JSONObject makePostRequest(String tag, String path, String token, String request
*
* @return the response body
*/
JSONObject makeUploadRequest(String tag, String path, String token, byte[] requestBody)
JSONObject makeUploadRequest(
String tag, String path, String token, RequestBodyWriter requestBodyWriter)
throws FirebaseAppDistributionException {
Map<String, String> extraHeaders = new HashMap<>();
extraHeaders.put(X_GOOG_UPLOAD_PROTOCOL_HEADER, X_GOOG_UPLOAD_PROTOCOL_RAW);
extraHeaders.put(X_GOOG_UPLOAD_FILE_NAME_HEADER, X_GOOG_UPLOAD_FILE_NAME);
return makePostRequest(tag, path, token, requestBody, extraHeaders);
return makePostRequest(tag, path, token, extraHeaders, requestBodyWriter);
}

private JSONObject makePostRequest(
String tag, String path, String token, byte[] requestBody, Map<String, String> extraHeaders)
String tag,
String path,
String token,
Map<String, String> extraHeaders,
RequestBodyWriter requestBodyWriter)
throws FirebaseAppDistributionException {
HttpsURLConnection connection = null;
try {
connection = openHttpsUrlConnection(getTesterApiUrl(path), token);
connection.setDoOutput(true);
connection.setRequestMethod(REQUEST_METHOD_POST);
connection.addRequestProperty(CONTENT_TYPE_HEADER_KEY, JSON_CONTENT_TYPE);
connection.addRequestProperty(CONTENT_ENCODING_HEADER_KEY, GZIP_CONTENT_ENCODING);
for (Map.Entry<String, String> e : extraHeaders.entrySet()) {
connection.addRequestProperty(e.getKey(), e.getValue());
}
GZIPOutputStream gzipOutputStream = new GZIPOutputStream(connection.getOutputStream());
OutputStream outputStream = connection.getOutputStream();
try {
gzipOutputStream.write(requestBody);
requestBodyWriter.write(outputStream);
} catch (IOException e) {
throw getException(tag, "Error compressing network request body", Status.UNKNOWN, e);
throw getException(tag, "Error writing network request body", Status.UNKNOWN, e);
} finally {
gzipOutputStream.close();
outputStream.close();
}
return readResponse(tag, connection);
} catch (IOException e) {
Expand Down
Loading