Skip to content

Commit bd94e28

Browse files
authored
Implement collectAndSendFeedback() (#3816)
* Add FeedbackActivity * Add FeedbackSender and tests * Update api.txt * Move FeedbackSender instantiation to getInstance() * Formatting * Add javadoc * Remove a @nullable annotation and an unnecessary dependency * Formatting
1 parent 0dd2be1 commit bd94e28

File tree

18 files changed

+440
-13
lines changed

18 files changed

+440
-13
lines changed

firebase-appdistribution-api/api.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ package com.google.firebase.appdistribution {
1515

1616
public interface FirebaseAppDistribution {
1717
method @NonNull public com.google.android.gms.tasks.Task<com.google.firebase.appdistribution.AppDistributionRelease> checkForNewRelease();
18+
method public void collectAndSendFeedback();
1819
method @NonNull public static com.google.firebase.appdistribution.FirebaseAppDistribution getInstance();
1920
method public boolean isTesterSignedIn();
2021
method @NonNull public com.google.android.gms.tasks.Task<java.lang.Void> signInTester();

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,20 @@ public interface FirebaseAppDistribution {
109109
@NonNull
110110
UpdateTask updateApp();
111111

112+
/**
113+
* Takes a screenshot, and starts an activity to collect and submit feedback from the tester.
114+
*
115+
* <p>Performs the following actions:
116+
*
117+
* <ol>
118+
* <li>Takes a screenshot of the current activity
119+
* <li>If tester is not signed in, presents the tester with a Google Sign-in UI
120+
* <li>Looks up the currently installed App Distribution release
121+
* <li>Starts a full screen activity for the tester to compose and submit the feedback
122+
* </ol>
123+
*/
124+
void collectAndSendFeedback();
125+
112126
/** Gets the singleton {@link FirebaseAppDistribution} instance. */
113127
@NonNull
114128
static FirebaseAppDistribution getInstance() {

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ public interface UpdateProgress {
2424
* @returns the number of bytes downloaded, or -1 if called when updating to an AAB or if no new
2525
* release is available.
2626
*/
27-
@NonNull
2827
long getApkBytesDownloaded();
2928

3029
/**
@@ -33,7 +32,6 @@ public interface UpdateProgress {
3332
* @returns the file size in bytes, or -1 if called when updating to an AAB or if no new release
3433
* is available.
3534
*/
36-
@NonNull
3735
long getApkFileTotalBytes();
3836

3937
/** Returns the current {@link UpdateStatus} of the update. */

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,9 @@ public synchronized Task<AppDistributionRelease> checkForNewRelease() {
7171
public UpdateTask updateApp() {
7272
return delegate.updateApp();
7373
}
74+
75+
@Override
76+
public void collectAndSendFeedback() {
77+
delegate.collectAndSendFeedback();
78+
}
7479
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ public UpdateTask updateApp() {
7373
return new NotImplementedUpdateTask();
7474
}
7575

76+
@Override
77+
public void collectAndSendFeedback() {
78+
return;
79+
}
80+
7681
private static <TResult> Task<TResult> getNotImplementedTask() {
7782
return Tasks.forException(
7883
new FirebaseAppDistributionException(

firebase-appdistribution/firebase-appdistribution.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,5 @@ dependencies {
6363
annotationProcessor 'com.google.auto.value:auto-value:1.6.5'
6464
implementation 'androidx.appcompat:appcompat:1.3.0'
6565
implementation "androidx.browser:browser:1.3.0"
66+
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
6667
}

firebase-appdistribution/src/main/AndroidManifest.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
android:name="com.google.firebase.components:com.google.firebase.appdistribution.impl.FirebaseAppDistributionRegistrar"
3030
android:value="com.google.firebase.components.ComponentRegistrar" />
3131
</service>
32+
3233
<!-- The launch mode for Install Activity is singleTask to ensure that after the unknown sources UI
3334
or the installation flow is complete, the Install Activity does not get recreated which causes loss of state
3435
See here for more info - https://developer.android.com/guide/components/activities/tasks-and-back-stack#ManifestForTasks -->
@@ -45,6 +46,11 @@
4546
</intent-filter>
4647
</activity>
4748

49+
<activity
50+
android:name=".FeedbackActivity"
51+
android:exported="false"
52+
android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
53+
4854
<provider
4955
android:name="com.google.firebase.appdistribution.impl.FirebaseAppDistributionFileProvider"
5056
android:authorities="${applicationId}.FirebaseAppDistributionFileProvider"

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class ErrorMessages {
2626
"Failed to authenticate the tester. The tester was either not signed in, or something went wrong. Try signing in again.";
2727

2828
static final String AUTHORIZATION_ERROR =
29-
"Failed to authorize the tester. The tester is not authorized to test this app. Verify that the tester has accepted an invitation to test this app.";
29+
"Failed to authorize the tester. The tester does not have access to this resource (or it may not exist).";
3030

3131
static final String AUTHENTICATION_CANCELED = "Tester canceled the authentication flow.";
3232

@@ -46,7 +46,7 @@ class ErrorMessages {
4646
"Download URL not found. This was a most likely due to a transient condition and may be corrected by retrying.";
4747

4848
static final String HOST_ACTIVITY_INTERRUPTED =
49-
"Host activity interrupted while dialog was showing. Try calling updateIfNewReleaseAvailable() again.";
49+
"Host activity interrupted while dialog was showing. Try calling the API again.";
5050

5151
static final String APK_INSTALLATION_FAILED =
5252
"The APK failed to install or installation was canceled by the tester.";
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Copyright 2022 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.appdistribution.impl;
16+
17+
import android.graphics.Bitmap;
18+
import android.os.Bundle;
19+
import android.view.View;
20+
import android.widget.EditText;
21+
import android.widget.Toast;
22+
import androidx.appcompat.app.AppCompatActivity;
23+
24+
/** Activity for tester to compose and submit feedback. */
25+
public class FeedbackActivity extends AppCompatActivity {
26+
27+
private static final String TAG = "FeedbackActivity";
28+
29+
public static final String RELEASE_NAME_EXTRA_KEY =
30+
"com.google.firebase.appdistribution.FeedbackActivity.RELEASE_NAME";
31+
public static final String SCREENSHOT_EXTRA_KEY =
32+
"com.google.firebase.appdistribution.FeedbackActivity.SCREENSHOT";
33+
34+
private FeedbackSender feedbackSender;
35+
private String releaseName;
36+
private Bitmap screenshot;
37+
38+
@Override
39+
protected void onCreate(Bundle savedInstanceState) {
40+
super.onCreate(savedInstanceState);
41+
releaseName = getIntent().getStringExtra(RELEASE_NAME_EXTRA_KEY);
42+
screenshot = getIntent().getParcelableExtra(SCREENSHOT_EXTRA_KEY);
43+
feedbackSender = FeedbackSender.getInstance();
44+
setContentView(R.layout.activity_feedback);
45+
}
46+
47+
public void submitFeedback(View view) {
48+
setSubmittingStateEnabled(true);
49+
EditText feedbackText = (EditText) findViewById(R.id.feedbackText);
50+
feedbackSender
51+
.sendFeedback(releaseName, feedbackText.getText().toString(), screenshot)
52+
.addOnSuccessListener(
53+
unused -> {
54+
LogWrapper.getInstance().i(TAG, "Feedback submitted");
55+
Toast.makeText(this, "Feedback submitted", Toast.LENGTH_LONG).show();
56+
finish();
57+
})
58+
.addOnFailureListener(
59+
e -> {
60+
LogWrapper.getInstance().e(TAG, "Failed to submit feedback", e);
61+
Toast.makeText(this, "Error submitting feedback", Toast.LENGTH_LONG).show();
62+
setSubmittingStateEnabled(false);
63+
});
64+
}
65+
66+
public void setSubmittingStateEnabled(boolean loading) {
67+
findViewById(R.id.submitButton).setVisibility(loading ? View.INVISIBLE : View.VISIBLE);
68+
findViewById(R.id.loadingLabel).setVisibility(loading ? View.VISIBLE : View.INVISIBLE);
69+
}
70+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright 2022 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.appdistribution.impl;
16+
17+
import android.graphics.Bitmap;
18+
import com.google.android.gms.tasks.Task;
19+
import com.google.firebase.FirebaseApp;
20+
21+
/** Sends tester feedback to the Tester API. */
22+
class FeedbackSender {
23+
24+
private final FirebaseAppDistributionTesterApiClient testerApiClient;
25+
26+
FeedbackSender(FirebaseAppDistributionTesterApiClient testerApiClient) {
27+
this.testerApiClient = testerApiClient;
28+
}
29+
30+
/** Get an instance of FeedbackSender. */
31+
static FeedbackSender getInstance() {
32+
return FirebaseApp.getInstance().get(FeedbackSender.class);
33+
}
34+
35+
/** Send feedback text and screenshot to the Tester API for the given release. */
36+
Task<Void> sendFeedback(String releaseName, String feedbackText, Bitmap screenshot) {
37+
return testerApiClient
38+
.createFeedback(releaseName, feedbackText)
39+
.onSuccessTask(feedbackName -> testerApiClient.attachScreenshot(feedbackName, screenshot))
40+
.onSuccessTask(testerApiClient::commitFeedback);
41+
}
42+
}

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

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,16 @@
1818
import static com.google.firebase.appdistribution.FirebaseAppDistributionException.Status.AUTHENTICATION_FAILURE;
1919
import static com.google.firebase.appdistribution.FirebaseAppDistributionException.Status.HOST_ACTIVITY_INTERRUPTED;
2020
import static com.google.firebase.appdistribution.FirebaseAppDistributionException.Status.UPDATE_NOT_AVAILABLE;
21+
import static com.google.firebase.appdistribution.impl.FeedbackActivity.RELEASE_NAME_EXTRA_KEY;
22+
import static com.google.firebase.appdistribution.impl.FeedbackActivity.SCREENSHOT_EXTRA_KEY;
2123
import static com.google.firebase.appdistribution.impl.TaskUtils.safeSetTaskException;
2224
import static com.google.firebase.appdistribution.impl.TaskUtils.safeSetTaskResult;
2325

2426
import android.app.Activity;
2527
import android.app.AlertDialog;
2628
import android.content.Context;
29+
import android.content.Intent;
30+
import android.graphics.Bitmap;
2731
import androidx.annotation.GuardedBy;
2832
import androidx.annotation.NonNull;
2933
import androidx.annotation.Nullable;
@@ -40,6 +44,8 @@
4044
import com.google.firebase.appdistribution.UpdateProgress;
4145
import com.google.firebase.appdistribution.UpdateStatus;
4246
import com.google.firebase.appdistribution.UpdateTask;
47+
import java.util.concurrent.Executor;
48+
import java.util.concurrent.Executors;
4349

4450
/**
4551
* This class is the "real" implementation of the Firebase App Distribution API which should only be
@@ -57,6 +63,7 @@ class FirebaseAppDistributionImpl implements FirebaseAppDistribution {
5763
private final AabUpdater aabUpdater;
5864
private final SignInStorage signInStorage;
5965
private final ReleaseIdentifier releaseIdentifier;
66+
private final ScreenshotTaker screenshotTaker;
6067

6168
private final Object updateIfNewReleaseTaskLock = new Object();
6269

@@ -88,7 +95,8 @@ class FirebaseAppDistributionImpl implements FirebaseAppDistribution {
8895
@NonNull AabUpdater aabUpdater,
8996
@NonNull SignInStorage signInStorage,
9097
@NonNull FirebaseAppDistributionLifecycleNotifier lifecycleNotifier,
91-
@NonNull ReleaseIdentifier releaseIdentifier) {
98+
@NonNull ReleaseIdentifier releaseIdentifier,
99+
@NonNull ScreenshotTaker screenshotTaker) {
92100
this.firebaseApp = firebaseApp;
93101
this.testerSignInManager = testerSignInManager;
94102
this.newReleaseFetcher = newReleaseFetcher;
@@ -97,6 +105,7 @@ class FirebaseAppDistributionImpl implements FirebaseAppDistribution {
97105
this.signInStorage = signInStorage;
98106
this.releaseIdentifier = releaseIdentifier;
99107
this.lifecycleNotifier = lifecycleNotifier;
108+
this.screenshotTaker = screenshotTaker;
100109
lifecycleNotifier.addOnActivityDestroyedListener(this::onActivityDestroyed);
101110
lifecycleNotifier.addOnActivityPausedListener(this::onActivityPaused);
102111
lifecycleNotifier.addOnActivityResumedListener(this::onActivityResumed);
@@ -297,6 +306,43 @@ private UpdateTask updateApp(boolean showDownloadInNotificationManager) {
297306
}
298307
}
299308

309+
@Override
310+
public void collectAndSendFeedback() {
311+
collectAndSendFeedback(Executors.newSingleThreadExecutor());
312+
}
313+
314+
@VisibleForTesting
315+
public void collectAndSendFeedback(Executor taskExecutor) {
316+
screenshotTaker
317+
.takeScreenshot()
318+
.onSuccessTask(
319+
taskExecutor,
320+
screenshot ->
321+
testerSignInManager
322+
.signInTester()
323+
.addOnFailureListener(
324+
taskExecutor,
325+
e ->
326+
LogWrapper.getInstance()
327+
.e("Failed to sign in tester. Could not collect feedback.", e))
328+
.onSuccessTask(taskExecutor, unused -> releaseIdentifier.identifyRelease())
329+
.onSuccessTask(
330+
taskExecutor,
331+
releaseName -> launchFeedbackActivity(releaseName, screenshot)))
332+
.addOnFailureListener(
333+
taskExecutor, e -> LogWrapper.getInstance().e("Failed to launch feedback flow", e));
334+
}
335+
336+
private Task<Void> launchFeedbackActivity(String releaseName, Bitmap screenshot) {
337+
return lifecycleNotifier.applyToForegroundActivity(
338+
activity -> {
339+
Intent intent = new Intent(activity, FeedbackActivity.class);
340+
intent.putExtra(RELEASE_NAME_EXTRA_KEY, releaseName);
341+
intent.putExtra(SCREENSHOT_EXTRA_KEY, screenshot);
342+
activity.startActivity(intent);
343+
});
344+
}
345+
300346
@VisibleForTesting
301347
void onActivityResumed(Activity activity) {
302348
if (awaitingSignInDialogConfirmation()) {

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

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,18 +51,33 @@ public class FirebaseAppDistributionRegistrar implements ComponentRegistrar {
5151
// activity lifecycle callbacks before the API is called
5252
.alwaysEager()
5353
.build(),
54+
Component.builder(FeedbackSender.class)
55+
.add(Dependency.required(FirebaseApp.class))
56+
.add(Dependency.requiredProvider(FirebaseInstallationsApi.class))
57+
.factory(this::buildFeedbackSender)
58+
.build(),
5459
LibraryVersionComponent.create("fire-appdistribution", BuildConfig.VERSION_NAME));
5560
}
5661

62+
private FeedbackSender buildFeedbackSender(ComponentContainer container) {
63+
FirebaseApp firebaseApp = container.get(FirebaseApp.class);
64+
Provider<FirebaseInstallationsApi> firebaseInstallationsApiProvider =
65+
container.getProvider(FirebaseInstallationsApi.class);
66+
FirebaseAppDistributionTesterApiClient testerApiClient =
67+
new FirebaseAppDistributionTesterApiClient(
68+
firebaseApp, firebaseInstallationsApiProvider, new TesterApiHttpClient(firebaseApp));
69+
return new FeedbackSender(testerApiClient);
70+
}
71+
5772
private FirebaseAppDistribution buildFirebaseAppDistribution(ComponentContainer container) {
5873
FirebaseApp firebaseApp = container.get(FirebaseApp.class);
5974
Context context = firebaseApp.getApplicationContext();
6075
Provider<FirebaseInstallationsApi> firebaseInstallationsApiProvider =
6176
container.getProvider(FirebaseInstallationsApi.class);
62-
SignInStorage signInStorage = new SignInStorage(context);
6377
FirebaseAppDistributionTesterApiClient testerApiClient =
6478
new FirebaseAppDistributionTesterApiClient(
6579
firebaseApp, firebaseInstallationsApiProvider, new TesterApiHttpClient(firebaseApp));
80+
SignInStorage signInStorage = new SignInStorage(context);
6681
FirebaseAppDistributionLifecycleNotifier lifecycleNotifier =
6782
FirebaseAppDistributionLifecycleNotifier.getInstance();
6883
ReleaseIdentifier releaseIdentifier = new ReleaseIdentifier(firebaseApp, testerApiClient);
@@ -76,7 +91,8 @@ private FirebaseAppDistribution buildFirebaseAppDistribution(ComponentContainer
7691
new AabUpdater(),
7792
signInStorage,
7893
lifecycleNotifier,
79-
releaseIdentifier);
94+
releaseIdentifier,
95+
new ScreenshotTaker());
8096

8197
if (context instanceof Application) {
8298
Application firebaseApplication = (Application) context;

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

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

1717
import static com.google.firebase.appdistribution.impl.PackageInfoUtils.getPackageInfoWithMetadata;
1818

19-
import android.content.Context;
2019
import android.content.pm.PackageInfo;
2120
import androidx.annotation.NonNull;
2221
import androidx.annotation.Nullable;
@@ -58,8 +57,6 @@ class ReleaseIdentifier {
5857

5958
/** Identify the currently installed release, returning the release name. */
6059
Task<String> identifyRelease() {
61-
Context context = firebaseApp.getApplicationContext();
62-
6360
// Attempt to find release using IAS artifact ID, which identifies app bundle releases
6461
String iasArtifactId = null;
6562
try {

0 commit comments

Comments
 (0)