Skip to content

Commit fe657de

Browse files
authored
Add a screenshot-based trigger for in-app feedback (#4122)
* WIP - Screenshot feedback trigger * Get screenshot trigger working * Some fixes and refactors * Format java * Resolve TODOs in SDK * Update api.txt * Address PR feedback and fix tests * Address Kai's feedback * Request different permission on 33+ and some more feedback
1 parent eb9414a commit fe657de

File tree

22 files changed

+521
-178
lines changed

22 files changed

+521
-178
lines changed

firebase-appdistribution-api/api.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ package com.google.firebase.appdistribution {
2121
method public void signOutTester();
2222
method public void startFeedback(int);
2323
method public void startFeedback(@NonNull CharSequence);
24+
method public void startFeedback(@NonNull int, @Nullable android.net.Uri);
25+
method public void startFeedback(@NonNull CharSequence, @Nullable android.net.Uri);
2426
method @NonNull public com.google.firebase.appdistribution.UpdateTask updateApp();
2527
method @NonNull public com.google.firebase.appdistribution.UpdateTask updateIfNewReleaseAvailable();
2628
}

firebase-appdistribution-api/src/main/java/com/google/firebase/appdistribution/FirebaseAppDistribution.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414

1515
package com.google.firebase.appdistribution;
1616

17+
import android.net.Uri;
1718
import androidx.annotation.NonNull;
19+
import androidx.annotation.Nullable;
1820
import com.google.android.gms.tasks.Task;
1921
import com.google.firebase.FirebaseApp;
2022
import com.google.firebase.appdistribution.internal.FirebaseAppDistributionProxy;
@@ -141,6 +143,42 @@ public interface FirebaseAppDistribution {
141143
*/
142144
void startFeedback(@NonNull CharSequence infoText);
143145

146+
/**
147+
* Starts an activity to collect and submit feedback from the tester, along with the given
148+
* screenshot.
149+
*
150+
* <p>Performs the following actions:
151+
*
152+
* <ol>
153+
* <li>If tester is not signed in, presents the tester with a Google Sign-in UI
154+
* <li>Starts a full screen activity for the tester to compose and submit the feedback
155+
* </ol>
156+
*
157+
* @param infoTextResourceId string resource ID of text to display to the tester before collecting
158+
* feedback data (e.g. Terms and Conditions)
159+
* @param screenshot URI to a bitmap containing a screenshot that will be included with the
160+
* report, or null to not include a screenshot
161+
*/
162+
void startFeedback(@NonNull int infoTextResourceId, @Nullable Uri screenshot);
163+
164+
/**
165+
* Starts an activity to collect and submit feedback from the tester, along with the given
166+
* screenshot.
167+
*
168+
* <p>Performs the following actions:
169+
*
170+
* <ol>
171+
* <li>If tester is not signed in, presents the tester with a Google Sign-in UI
172+
* <li>Starts a full screen activity for the tester to compose and submit the feedback
173+
* </ol>
174+
*
175+
* @param infoText text to display to the tester before collecting feedback data (e.g. Terms and
176+
* Conditions)
177+
* @param screenshot URI to a bitmap containing a screenshot that will be included with the
178+
* report, or null to not include a screenshot
179+
*/
180+
void startFeedback(@NonNull CharSequence infoText, @Nullable Uri screenshotUri);
181+
144182
/** Gets the singleton {@link FirebaseAppDistribution} instance. */
145183
@NonNull
146184
static FirebaseAppDistribution getInstance() {

firebase-appdistribution-api/src/main/java/com/google/firebase/appdistribution/internal/FirebaseAppDistributionProxy.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414

1515
package com.google.firebase.appdistribution.internal;
1616

17+
import android.net.Uri;
1718
import androidx.annotation.NonNull;
19+
import androidx.annotation.Nullable;
1820
import com.google.android.gms.tasks.Task;
1921
import com.google.firebase.appdistribution.AppDistributionRelease;
2022
import com.google.firebase.appdistribution.FirebaseAppDistribution;
@@ -81,4 +83,14 @@ public void startFeedback(int infoTextResourceId) {
8183
public void startFeedback(@NonNull CharSequence infoText) {
8284
delegate.startFeedback(infoText);
8385
}
86+
87+
@Override
88+
public void startFeedback(@NonNull int infoTextResourceId, @Nullable Uri screenshotUri) {
89+
delegate.startFeedback(infoTextResourceId, screenshotUri);
90+
}
91+
92+
@Override
93+
public void startFeedback(@NonNull CharSequence infoText, @Nullable Uri screenshotUri) {
94+
delegate.startFeedback(infoText, screenshotUri);
95+
}
8496
}

firebase-appdistribution-api/src/main/java/com/google/firebase/appdistribution/internal/FirebaseAppDistributionStub.java

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package com.google.firebase.appdistribution.internal;
1616

1717
import android.app.Activity;
18+
import android.net.Uri;
1819
import androidx.annotation.NonNull;
1920
import androidx.annotation.Nullable;
2021
import com.google.android.gms.tasks.Continuation;
@@ -57,9 +58,7 @@ public Task<Void> signInTester() {
5758
}
5859

5960
@Override
60-
public void signOutTester() {
61-
return;
62-
}
61+
public void signOutTester() {}
6362

6463
@NonNull
6564
@Override
@@ -74,14 +73,16 @@ public UpdateTask updateApp() {
7473
}
7574

7675
@Override
77-
public void startFeedback(int infoTextResourceId) {
78-
return;
79-
}
76+
public void startFeedback(int infoTextResourceId) {}
8077

8178
@Override
82-
public void startFeedback(@NonNull CharSequence infoText) {
83-
return;
84-
}
79+
public void startFeedback(@NonNull CharSequence infoText) {}
80+
81+
@Override
82+
public void startFeedback(@NonNull int infoTextResourceId, @Nullable Uri screenshotUri) {}
83+
84+
@Override
85+
public void startFeedback(@NonNull CharSequence infoText, @Nullable Uri screenshotUri) {}
8586

8687
private static <TResult> Task<TResult> getNotImplementedTask() {
8788
return Tasks.forException(

firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/FeedbackActivity.java

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package com.google.firebase.appdistribution.impl;
1616

1717
import android.graphics.Bitmap;
18+
import android.net.Uri;
1819
import android.os.Bundle;
1920
import android.text.method.LinkMovementMethod;
2021
import android.view.View;
@@ -25,7 +26,7 @@
2526
import android.widget.Toast;
2627
import androidx.annotation.Nullable;
2728
import androidx.appcompat.app.AppCompatActivity;
28-
import java.io.File;
29+
import java.io.IOException;
2930

3031
/** Activity for tester to compose and submit feedback. */
3132
public class FeedbackActivity extends AppCompatActivity {
@@ -38,21 +39,21 @@ public class FeedbackActivity extends AppCompatActivity {
3839
"com.google.firebase.appdistribution.FeedbackActivity.RELEASE_NAME";
3940
public static final String INFO_TEXT_EXTRA_KEY =
4041
"com.google.firebase.appdistribution.FeedbackActivity.INFO_TEXT";
41-
public static final String SCREENSHOT_FILENAME_EXTRA_KEY =
42-
"com.google.firebase.appdistribution.FeedbackActivity.SCREENSHOT_FILE_NAME";
42+
public static final String SCREENSHOT_URI_EXTRA_KEY =
43+
"com.google.firebase.appdistribution.FeedbackActivity.SCREENSHOT_URI";
4344

4445
private FeedbackSender feedbackSender;
4546
private String releaseName;
4647
private CharSequence infoText;
47-
@Nullable private File screenshotFile;
48+
@Nullable private Uri screenshotUri;
4849

4950
@Override
5051
protected void onCreate(Bundle savedInstanceState) {
5152
super.onCreate(savedInstanceState);
5253
releaseName = getIntent().getStringExtra(RELEASE_NAME_EXTRA_KEY);
5354
infoText = getIntent().getCharSequenceExtra(INFO_TEXT_EXTRA_KEY);
54-
if (getIntent().hasExtra(SCREENSHOT_FILENAME_EXTRA_KEY)) {
55-
screenshotFile = getFileStreamPath(getIntent().getStringExtra(SCREENSHOT_FILENAME_EXTRA_KEY));
55+
if (getIntent().hasExtra(SCREENSHOT_URI_EXTRA_KEY)) {
56+
screenshotUri = Uri.parse(getIntent().getStringExtra(SCREENSHOT_URI_EXTRA_KEY));
5657
}
5758
feedbackSender = FeedbackSender.getInstance();
5859
setupView();
@@ -67,29 +68,40 @@ private void setupView() {
6768
Button submitButton = this.findViewById(R.id.submitButton);
6869
submitButton.setOnClickListener(this::submitFeedback);
6970

70-
Bitmap thumbnail = readThumbnail();
71+
Bitmap thumbnail = screenshotUri == null ? null : readThumbnail();
7172
if (thumbnail != null) {
7273
ImageView screenshotImageView = this.findViewById(R.id.thumbnail);
7374
screenshotImageView.setImageBitmap(thumbnail);
7475
} else {
76+
LogWrapper.getInstance().e(TAG, "No screenshot available");
7577
View screenshotErrorLabel = this.findViewById(R.id.screenshotErrorLabel);
7678
screenshotErrorLabel.setVisibility(View.VISIBLE);
7779
}
7880
}
7981

8082
@Nullable
8183
private Bitmap readThumbnail() {
82-
if (screenshotFile == null) {
84+
Bitmap thumbnail;
85+
try {
86+
thumbnail =
87+
ImageUtils.readScaledImage(
88+
getContentResolver(), screenshotUri, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT);
89+
} catch (IOException e) {
90+
LogWrapper.getInstance()
91+
.e(TAG, "Could not read screenshot image from URI: " + screenshotUri, e);
8392
return null;
8493
}
85-
return ImageUtils.readScaledImage(screenshotFile, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT);
94+
if (thumbnail == null) {
95+
LogWrapper.getInstance().e(TAG, "Could not decode screenshot image: " + screenshotUri);
96+
}
97+
return thumbnail;
8698
}
8799

88100
public void submitFeedback(View view) {
89101
setSubmittingStateEnabled(true);
90102
EditText feedbackText = findViewById(R.id.feedbackText);
91103
feedbackSender
92-
.sendFeedback(releaseName, feedbackText.getText().toString(), screenshotFile)
104+
.sendFeedback(releaseName, feedbackText.getText().toString(), screenshotUri)
93105
.addOnSuccessListener(
94106
unused -> {
95107
LogWrapper.getInstance().i(TAG, "Feedback submitted");

firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/FeedbackSender.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@
1414

1515
package com.google.firebase.appdistribution.impl;
1616

17+
import android.net.Uri;
1718
import androidx.annotation.Nullable;
1819
import com.google.android.gms.tasks.Task;
1920
import com.google.android.gms.tasks.Tasks;
2021
import com.google.firebase.FirebaseApp;
21-
import java.io.File;
2222

2323
/** Sends tester feedback to the Tester API. */
2424
class FeedbackSender {
@@ -35,17 +35,17 @@ static FeedbackSender getInstance() {
3535
}
3636

3737
/** Send feedback text and optionally a screenshot to the Tester API for the given release. */
38-
Task<Void> sendFeedback(String releaseName, String feedbackText, @Nullable File screenshotFile) {
38+
Task<Void> sendFeedback(String releaseName, String feedbackText, @Nullable Uri screenshotUri) {
3939
return testerApiClient
4040
.createFeedback(releaseName, feedbackText)
41-
.onSuccessTask(feedbackName -> attachScreenshot(feedbackName, screenshotFile))
41+
.onSuccessTask(feedbackName -> attachScreenshot(feedbackName, screenshotUri))
4242
.onSuccessTask(testerApiClient::commitFeedback);
4343
}
4444

45-
private Task<String> attachScreenshot(String feedbackName, @Nullable File screenshotFile) {
46-
if (screenshotFile == null) {
45+
private Task<String> attachScreenshot(String feedbackName, @Nullable Uri screenshotUri) {
46+
if (screenshotUri == null) {
4747
return Tasks.forResult(feedbackName);
4848
}
49-
return testerApiClient.attachScreenshot(feedbackName, screenshotFile);
49+
return testerApiClient.attachScreenshot(feedbackName, screenshotUri);
5050
}
5151
}

firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/FirebaseAppDistributionImpl.java

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,15 @@
2020
import static com.google.firebase.appdistribution.FirebaseAppDistributionException.Status.UPDATE_NOT_AVAILABLE;
2121
import static com.google.firebase.appdistribution.impl.FeedbackActivity.INFO_TEXT_EXTRA_KEY;
2222
import static com.google.firebase.appdistribution.impl.FeedbackActivity.RELEASE_NAME_EXTRA_KEY;
23-
import static com.google.firebase.appdistribution.impl.FeedbackActivity.SCREENSHOT_FILENAME_EXTRA_KEY;
23+
import static com.google.firebase.appdistribution.impl.FeedbackActivity.SCREENSHOT_URI_EXTRA_KEY;
2424
import static com.google.firebase.appdistribution.impl.TaskUtils.safeSetTaskException;
2525
import static com.google.firebase.appdistribution.impl.TaskUtils.safeSetTaskResult;
2626

2727
import android.app.Activity;
2828
import android.app.AlertDialog;
2929
import android.content.Context;
3030
import android.content.Intent;
31+
import android.net.Uri;
3132
import androidx.annotation.GuardedBy;
3233
import androidx.annotation.NonNull;
3334
import androidx.annotation.Nullable;
@@ -315,37 +316,53 @@ public void startFeedback(int infoTextResourceId) {
315316
startFeedback(firebaseApp.getApplicationContext().getText(infoTextResourceId));
316317
}
317318

318-
@VisibleForTesting
319+
@Override
319320
public void startFeedback(@NonNull CharSequence infoText) {
320321
screenshotTaker
321322
.takeScreenshot()
323+
.addOnFailureListener(
324+
taskExecutor,
325+
e -> {
326+
LogWrapper.getInstance().w("Failed to take screenshot for feedback", e);
327+
startFeedback(infoText, null);
328+
})
329+
.addOnSuccessListener(
330+
taskExecutor, screenshotUri -> startFeedback(infoText, screenshotUri));
331+
}
332+
333+
@Override
334+
public void startFeedback(@NonNull int infoTextResourceId, @Nullable Uri screenshotUri) {
335+
startFeedback(firebaseApp.getApplicationContext().getText(infoTextResourceId), screenshotUri);
336+
}
337+
338+
@Override
339+
public void startFeedback(@NonNull CharSequence infoText, @Nullable Uri screenshotUri) {
340+
testerSignInManager
341+
.signInTester()
342+
.addOnFailureListener(
343+
taskExecutor,
344+
e ->
345+
LogWrapper.getInstance()
346+
.e("Failed to sign in tester. Could not collect feedback.", e))
347+
.onSuccessTask(taskExecutor, unused -> releaseIdentifier.identifyRelease())
322348
.onSuccessTask(
323349
taskExecutor,
324-
screenshotFilename ->
325-
testerSignInManager
326-
.signInTester()
327-
.addOnFailureListener(
328-
taskExecutor,
329-
e ->
330-
LogWrapper.getInstance()
331-
.e("Failed to sign in tester. Could not collect feedback.", e))
332-
.onSuccessTask(taskExecutor, unused -> releaseIdentifier.identifyRelease())
333-
.onSuccessTask(
334-
taskExecutor,
335-
releaseName ->
336-
launchFeedbackActivity(releaseName, infoText, screenshotFilename)))
350+
releaseName -> launchFeedbackActivity(releaseName, infoText, screenshotUri))
337351
.addOnFailureListener(
338352
taskExecutor, e -> LogWrapper.getInstance().e("Failed to launch feedback flow", e));
339353
}
340354

341355
private Task<Void> launchFeedbackActivity(
342-
String releaseName, CharSequence infoText, String screenshotFilename) {
356+
String releaseName, CharSequence infoText, @Nullable Uri screenshotUri) {
343357
return lifecycleNotifier.consumeForegroundActivity(
344358
activity -> {
359+
LogWrapper.getInstance().i("Launching feedback activity");
345360
Intent intent = new Intent(activity, FeedbackActivity.class);
346361
intent.putExtra(RELEASE_NAME_EXTRA_KEY, releaseName);
347362
intent.putExtra(INFO_TEXT_EXTRA_KEY, infoText);
348-
intent.putExtra(SCREENSHOT_FILENAME_EXTRA_KEY, screenshotFilename);
363+
if (screenshotUri != null) {
364+
intent.putExtra(SCREENSHOT_URI_EXTRA_KEY, screenshotUri.toString());
365+
}
349366
activity.startActivity(intent);
350367
});
351368
}

firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/FirebaseAppDistributionTesterApiClient.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import static com.google.firebase.appdistribution.impl.TaskUtils.runAsyncInTask;
1818

19+
import android.net.Uri;
1920
import androidx.annotation.NonNull;
2021
import com.google.android.gms.tasks.Task;
2122
import com.google.android.gms.tasks.Tasks;
@@ -26,7 +27,6 @@
2627
import com.google.firebase.inject.Provider;
2728
import com.google.firebase.installations.FirebaseInstallationsApi;
2829
import com.google.firebase.installations.InstallationTokenResult;
29-
import java.io.File;
3030
import java.util.concurrent.Executor;
3131
import java.util.concurrent.Executors;
3232
import org.json.JSONArray;
@@ -150,13 +150,13 @@ Task<String> createFeedback(String testerReleaseName, String feedbackText) {
150150
* @return a {@link Task} containing the feedback name, for convenience when chaining subsequent
151151
* requests off of this task
152152
*/
153-
Task<String> attachScreenshot(String feedbackName, File screenshotFile) {
153+
Task<String> attachScreenshot(String feedbackName, Uri screenshotUri) {
154154
return runWithFidAndToken(
155155
(unused, token) -> {
156156
LogWrapper.getInstance().i("Uploading screenshot for feedback: " + feedbackName);
157157
String path =
158158
String.format("upload/v1alpha/%s:uploadArtifact?type=SCREENSHOT", feedbackName);
159-
testerApiHttpClient.makeUploadRequest(UPLOAD_SCREENSHOT_TAG, path, token, screenshotFile);
159+
testerApiHttpClient.makeUploadRequest(UPLOAD_SCREENSHOT_TAG, path, token, screenshotUri);
160160
return feedbackName;
161161
});
162162
}

0 commit comments

Comments
 (0)