Skip to content

Commit bd18723

Browse files
committed
Revert "Remove In-App Feedback functionality from firebase-appdistribution (#3926)" (#3927)
This reverts commit 78d2888.
1 parent 54e2b0c commit bd18723

File tree

16 files changed

+929
-3
lines changed

16 files changed

+929
-3
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/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/src/main/AndroidManifest.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@
4646
</intent-filter>
4747
</activity>
4848

49+
<activity
50+
android:name=".FeedbackActivity"
51+
android:exported="false"
52+
android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
53+
4954
<provider
5055
android:name="com.google.firebase.appdistribution.impl.FirebaseAppDistributionFileProvider"
5156
android:authorities="${applicationId}.FirebaseAppDistributionFileProvider"
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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.ImageView;
22+
import android.widget.Toast;
23+
import androidx.annotation.Nullable;
24+
import androidx.appcompat.app.AppCompatActivity;
25+
import java.io.File;
26+
27+
/** Activity for tester to compose and submit feedback. */
28+
public class FeedbackActivity extends AppCompatActivity {
29+
30+
private static final String TAG = "FeedbackActivity";
31+
private static final int THUMBNAIL_WIDTH = 200;
32+
private static final int THUMBNAIL_HEIGHT = 200;
33+
34+
public static final String RELEASE_NAME_EXTRA_KEY =
35+
"com.google.firebase.appdistribution.FeedbackActivity.RELEASE_NAME";
36+
public static final String SCREENSHOT_FILENAME_EXTRA_KEY =
37+
"com.google.firebase.appdistribution.FeedbackActivity.SCREENSHOT_FILE_NAME";
38+
39+
private FeedbackSender feedbackSender;
40+
private String releaseName;
41+
@Nullable private File screenshotFile;
42+
43+
@Override
44+
protected void onCreate(Bundle savedInstanceState) {
45+
super.onCreate(savedInstanceState);
46+
releaseName = getIntent().getStringExtra(RELEASE_NAME_EXTRA_KEY);
47+
if (getIntent().hasExtra(SCREENSHOT_FILENAME_EXTRA_KEY)) {
48+
screenshotFile = getFileStreamPath(getIntent().getStringExtra(SCREENSHOT_FILENAME_EXTRA_KEY));
49+
}
50+
feedbackSender = FeedbackSender.getInstance();
51+
setupView();
52+
}
53+
54+
private void setupView() {
55+
setContentView(R.layout.activity_feedback);
56+
Bitmap thumbnail = readThumbnail();
57+
if (thumbnail != null) {
58+
ImageView screenshotImageView = (ImageView) this.findViewById(R.id.thumbnail);
59+
screenshotImageView.setImageBitmap(thumbnail);
60+
} else {
61+
View screenshotErrorLabel = this.findViewById(R.id.screenshotErrorLabel);
62+
screenshotErrorLabel.setVisibility(View.VISIBLE);
63+
}
64+
}
65+
66+
@Nullable
67+
private Bitmap readThumbnail() {
68+
if (screenshotFile == null) {
69+
return null;
70+
}
71+
return ImageUtils.readScaledImage(screenshotFile, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT);
72+
}
73+
74+
public void submitFeedback(View view) {
75+
setSubmittingStateEnabled(true);
76+
EditText feedbackText = (EditText) findViewById(R.id.feedbackText);
77+
feedbackSender
78+
.sendFeedback(releaseName, feedbackText.getText().toString(), screenshotFile)
79+
.addOnSuccessListener(
80+
unused -> {
81+
LogWrapper.getInstance().i(TAG, "Feedback submitted");
82+
Toast.makeText(this, "Feedback submitted", Toast.LENGTH_LONG).show();
83+
finish();
84+
})
85+
.addOnFailureListener(
86+
e -> {
87+
LogWrapper.getInstance().e(TAG, "Failed to submit feedback", e);
88+
Toast.makeText(this, "Error submitting feedback", Toast.LENGTH_LONG).show();
89+
setSubmittingStateEnabled(false);
90+
});
91+
}
92+
93+
public void setSubmittingStateEnabled(boolean loading) {
94+
findViewById(R.id.submitButton).setVisibility(loading ? View.INVISIBLE : View.VISIBLE);
95+
findViewById(R.id.loadingLabel).setVisibility(loading ? View.VISIBLE : View.INVISIBLE);
96+
}
97+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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 androidx.annotation.Nullable;
18+
import com.google.android.gms.tasks.Task;
19+
import com.google.android.gms.tasks.Tasks;
20+
import com.google.firebase.FirebaseApp;
21+
import java.io.File;
22+
23+
/** Sends tester feedback to the Tester API. */
24+
class FeedbackSender {
25+
26+
private final FirebaseAppDistributionTesterApiClient testerApiClient;
27+
28+
FeedbackSender(FirebaseAppDistributionTesterApiClient testerApiClient) {
29+
this.testerApiClient = testerApiClient;
30+
}
31+
32+
/** Get an instance of FeedbackSender. */
33+
static FeedbackSender getInstance() {
34+
return FirebaseApp.getInstance().get(FeedbackSender.class);
35+
}
36+
37+
/** 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) {
39+
return testerApiClient
40+
.createFeedback(releaseName, feedbackText)
41+
.onSuccessTask(feedbackName -> attachScreenshot(feedbackName, screenshotFile))
42+
.onSuccessTask(testerApiClient::commitFeedback);
43+
}
44+
45+
private Task<String> attachScreenshot(String feedbackName, @Nullable File screenshotFile) {
46+
if (screenshotFile == null) {
47+
return Tasks.forResult(feedbackName);
48+
}
49+
return testerApiClient.attachScreenshot(feedbackName, screenshotFile);
50+
}
51+
}

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

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,15 @@
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_FILENAME_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;
2730
import androidx.annotation.GuardedBy;
2831
import androidx.annotation.NonNull;
2932
import androidx.annotation.Nullable;
@@ -40,6 +43,8 @@
4043
import com.google.firebase.appdistribution.UpdateProgress;
4144
import com.google.firebase.appdistribution.UpdateStatus;
4245
import com.google.firebase.appdistribution.UpdateTask;
46+
import java.util.concurrent.Executor;
47+
import java.util.concurrent.Executors;
4348

4449
/**
4550
* This class is the "real" implementation of the Firebase App Distribution API which should only be
@@ -56,6 +61,8 @@ class FirebaseAppDistributionImpl implements FirebaseAppDistribution {
5661
private final ApkUpdater apkUpdater;
5762
private final AabUpdater aabUpdater;
5863
private final SignInStorage signInStorage;
64+
private final ReleaseIdentifier releaseIdentifier;
65+
private final ScreenshotTaker screenshotTaker;
5966

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

@@ -86,14 +93,18 @@ class FirebaseAppDistributionImpl implements FirebaseAppDistribution {
8693
@NonNull ApkUpdater apkUpdater,
8794
@NonNull AabUpdater aabUpdater,
8895
@NonNull SignInStorage signInStorage,
89-
@NonNull FirebaseAppDistributionLifecycleNotifier lifecycleNotifier) {
96+
@NonNull FirebaseAppDistributionLifecycleNotifier lifecycleNotifier,
97+
@NonNull ReleaseIdentifier releaseIdentifier,
98+
@NonNull ScreenshotTaker screenshotTaker) {
9099
this.firebaseApp = firebaseApp;
91100
this.testerSignInManager = testerSignInManager;
92101
this.newReleaseFetcher = newReleaseFetcher;
93102
this.apkUpdater = apkUpdater;
94103
this.aabUpdater = aabUpdater;
95104
this.signInStorage = signInStorage;
105+
this.releaseIdentifier = releaseIdentifier;
96106
this.lifecycleNotifier = lifecycleNotifier;
107+
this.screenshotTaker = screenshotTaker;
97108
lifecycleNotifier.addOnActivityDestroyedListener(this::onActivityDestroyed);
98109
lifecycleNotifier.addOnActivityPausedListener(this::onActivityPaused);
99110
lifecycleNotifier.addOnActivityResumedListener(this::onActivityResumed);
@@ -294,6 +305,43 @@ private UpdateTask updateApp(boolean showDownloadInNotificationManager) {
294305
}
295306
}
296307

308+
@Override
309+
public void collectAndSendFeedback() {
310+
collectAndSendFeedback(Executors.newSingleThreadExecutor());
311+
}
312+
313+
@VisibleForTesting
314+
public void collectAndSendFeedback(Executor taskExecutor) {
315+
screenshotTaker
316+
.takeScreenshot()
317+
.onSuccessTask(
318+
taskExecutor,
319+
screenshotFilename ->
320+
testerSignInManager
321+
.signInTester()
322+
.addOnFailureListener(
323+
taskExecutor,
324+
e ->
325+
LogWrapper.getInstance()
326+
.e("Failed to sign in tester. Could not collect feedback.", e))
327+
.onSuccessTask(taskExecutor, unused -> releaseIdentifier.identifyRelease())
328+
.onSuccessTask(
329+
taskExecutor,
330+
releaseName -> launchFeedbackActivity(releaseName, screenshotFilename)))
331+
.addOnFailureListener(
332+
taskExecutor, e -> LogWrapper.getInstance().e("Failed to launch feedback flow", e));
333+
}
334+
335+
private Task<Void> launchFeedbackActivity(String releaseName, String screenshotFilename) {
336+
return lifecycleNotifier.consumeForegroundActivity(
337+
activity -> {
338+
Intent intent = new Intent(activity, FeedbackActivity.class);
339+
intent.putExtra(RELEASE_NAME_EXTRA_KEY, releaseName);
340+
intent.putExtra(SCREENSHOT_FILENAME_EXTRA_KEY, screenshotFilename);
341+
activity.startActivity(intent);
342+
});
343+
}
344+
297345
@VisibleForTesting
298346
void onActivityResumed(Activity activity) {
299347
if (awaitingSignInDialogConfirmation()) {
@@ -338,6 +386,13 @@ void onActivityDestroyed(@NonNull Activity activity) {
338386
if (activity == dialogHostActivity) {
339387
dialogHostActivity = null;
340388
}
389+
390+
// If the feedback activity finishes, clean up the screenshot that was taken before starting
391+
// the activity. If this does not happen for some reason it will be cleaned up the next time
392+
// before taking a new screenshot.
393+
if (activity instanceof FeedbackActivity) {
394+
screenshotTaker.deleteScreenshot();
395+
}
341396
}
342397

343398
@VisibleForTesting

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,24 @@ 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();
@@ -75,7 +90,9 @@ private FirebaseAppDistribution buildFirebaseAppDistribution(ComponentContainer
7590
new ApkUpdater(firebaseApp, new ApkInstaller()),
7691
new AabUpdater(),
7792
signInStorage,
78-
lifecycleNotifier);
93+
lifecycleNotifier,
94+
releaseIdentifier,
95+
new ScreenshotTaker(firebaseApp, lifecycleNotifier));
7996

8097
if (context instanceof Application) {
8198
Application firebaseApplication = (Application) context;

0 commit comments

Comments
 (0)