Skip to content

Commit 792e775

Browse files
committed
Add FeedbackActivity
1 parent a2502fe commit 792e775

File tree

15 files changed

+246
-13
lines changed

15 files changed

+246
-13
lines changed

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

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

112+
void collectAndSendFeedback();
113+
112114
/** Gets the singleton {@link FirebaseAppDistribution} instance. */
113115
@NonNull
114116
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/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: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package com.google.firebase.appdistribution.impl;
2+
3+
import android.graphics.Bitmap;
4+
import android.os.Bundle;
5+
import android.view.View;
6+
import android.widget.EditText;
7+
import android.widget.Toast;
8+
import androidx.appcompat.app.AppCompatActivity;
9+
import com.google.firebase.FirebaseApp;
10+
11+
/** Activity for tester to compose and submit feedback. */
12+
public class FeedbackActivity extends AppCompatActivity {
13+
14+
private static final String TAG = "FeedbackActivity";
15+
16+
public static final String RELEASE_NAME_EXTRA_KEY =
17+
"com.google.firebase.appdistribution.FeedbackActivity.RELEASE_NAME";
18+
public static final String SCREENSHOT_EXTRA_KEY =
19+
"com.google.firebase.appdistribution.FeedbackActivity.SCREENSHOT";
20+
21+
private FirebaseAppDistributionTesterApiClient testerApiClient;
22+
private String releaseName;
23+
private Bitmap screenshot;
24+
25+
@Override
26+
protected void onCreate(Bundle savedInstanceState) {
27+
super.onCreate(savedInstanceState);
28+
releaseName = getIntent().getStringExtra(RELEASE_NAME_EXTRA_KEY);
29+
screenshot = getIntent().getParcelableExtra(SCREENSHOT_EXTRA_KEY);
30+
testerApiClient = FirebaseApp.getInstance().get(FirebaseAppDistributionTesterApiClient.class);
31+
setContentView(R.layout.activity_feedback);
32+
}
33+
34+
public void submitFeedback(View view) {
35+
setSubmittingStateEnabled(true);
36+
EditText feedbackText = (EditText) findViewById(R.id.feedbackText);
37+
testerApiClient
38+
.createFeedback(releaseName, feedbackText.getText().toString())
39+
.onSuccessTask(feedbackName -> testerApiClient.attachScreenshot(feedbackName, screenshot))
40+
.onSuccessTask(testerApiClient::commitFeedback)
41+
.addOnSuccessListener(
42+
unused -> {
43+
LogWrapper.getInstance().i(TAG, "Feedback submitted");
44+
Toast.makeText(this, "Feedback submitted", Toast.LENGTH_LONG).show();
45+
finish();
46+
})
47+
.addOnFailureListener(
48+
e -> {
49+
LogWrapper.getInstance().e(TAG, "Failed to submit feedback", e);
50+
Toast.makeText(this, "Error submitting feedback", Toast.LENGTH_LONG).show();
51+
setSubmittingStateEnabled(false);
52+
});
53+
}
54+
55+
public void setSubmittingStateEnabled(boolean loading) {
56+
findViewById(R.id.submitButton).setVisibility(loading ? View.INVISIBLE : View.VISIBLE);
57+
findViewById(R.id.loadingLabel).setVisibility(loading ? View.VISIBLE : View.INVISIBLE);
58+
}
59+
}

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

Lines changed: 43 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,39 @@ 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.takeScreenshot()
317+
.onSuccessTask(
318+
taskExecutor,
319+
screenshot ->
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(taskExecutor, releaseName -> launchFeedbackActivity(releaseName, screenshot)))
329+
.addOnFailureListener(taskExecutor, e -> LogWrapper.getInstance().e("Failed to launch feedback flow", e));
330+
}
331+
332+
private Task<Void> launchFeedbackActivity(String releaseName, Bitmap screenshot) {
333+
return lifecycleNotifier.applyToForegroundActivity(
334+
activity -> {
335+
Intent intent = new Intent(activity, FeedbackActivity.class);
336+
intent.putExtra(RELEASE_NAME_EXTRA_KEY, releaseName);
337+
intent.putExtra(SCREENSHOT_EXTRA_KEY, screenshot);
338+
activity.startActivity(intent);
339+
});
340+
}
341+
300342
@VisibleForTesting
301343
void onActivityResumed(Activity activity) {
302344
if (awaitingSignInDialogConfirmation()) {

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

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,23 +46,37 @@ public class FirebaseAppDistributionRegistrar implements ComponentRegistrar {
4646
Component.builder(FirebaseAppDistribution.class)
4747
.add(Dependency.required(FirebaseApp.class))
4848
.add(Dependency.requiredProvider(FirebaseInstallationsApi.class))
49+
.add(Dependency.required(FirebaseAppDistributionTesterApiClient.class))
4950
.factory(this::buildFirebaseAppDistribution)
5051
// construct FirebaseAppDistribution instance on startup so we can register for
5152
// activity lifecycle callbacks before the API is called
5253
.alwaysEager()
5354
.build(),
55+
Component.builder(FirebaseAppDistributionTesterApiClient.class)
56+
.add(Dependency.required(FirebaseApp.class))
57+
.add(Dependency.requiredProvider(FirebaseInstallationsApi.class))
58+
.factory(this::buildFirebaseAppDistributionTesterApiClient)
59+
.build(),
5460
LibraryVersionComponent.create("fire-appdistribution", BuildConfig.VERSION_NAME));
5561
}
5662

63+
private FirebaseAppDistributionTesterApiClient buildFirebaseAppDistributionTesterApiClient(
64+
ComponentContainer container) {
65+
FirebaseApp firebaseApp = container.get(FirebaseApp.class);
66+
Provider<FirebaseInstallationsApi> firebaseInstallationsApiProvider =
67+
container.getProvider(FirebaseInstallationsApi.class);
68+
return new FirebaseAppDistributionTesterApiClient(
69+
firebaseApp, firebaseInstallationsApiProvider, new TesterApiHttpClient(firebaseApp));
70+
}
71+
5772
private FirebaseAppDistribution buildFirebaseAppDistribution(ComponentContainer container) {
5873
FirebaseApp firebaseApp = container.get(FirebaseApp.class);
74+
FirebaseAppDistributionTesterApiClient testerApiClient =
75+
container.get(FirebaseAppDistributionTesterApiClient.class);
5976
Context context = firebaseApp.getApplicationContext();
6077
Provider<FirebaseInstallationsApi> firebaseInstallationsApiProvider =
6178
container.getProvider(FirebaseInstallationsApi.class);
6279
SignInStorage signInStorage = new SignInStorage(context);
63-
FirebaseAppDistributionTesterApiClient testerApiClient =
64-
new FirebaseAppDistributionTesterApiClient(
65-
firebaseApp, firebaseInstallationsApiProvider, new TesterApiHttpClient(firebaseApp));
6680
FirebaseAppDistributionLifecycleNotifier lifecycleNotifier =
6781
FirebaseAppDistributionLifecycleNotifier.getInstance();
6882
ReleaseIdentifier releaseIdentifier = new ReleaseIdentifier(firebaseApp, testerApiClient);
@@ -76,7 +90,8 @@ private FirebaseAppDistribution buildFirebaseAppDistribution(ComponentContainer
7690
new AabUpdater(),
7791
signInStorage,
7892
lifecycleNotifier,
79-
releaseIdentifier);
93+
releaseIdentifier,
94+
new ScreenshotTaker());
8095

8196
if (context instanceof Application) {
8297
Application firebaseApplication = (Application) context;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import com.google.firebase.inject.Provider;
2929
import com.google.firebase.installations.FirebaseInstallationsApi;
3030
import com.google.firebase.installations.InstallationTokenResult;
31+
import java.io.ByteArrayOutputStream;
3132
import java.util.concurrent.Executor;
3233
import java.util.concurrent.Executors;
3334
import org.json.JSONArray;

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 {
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.google.firebase.appdistribution.impl;
2+
3+
import android.graphics.Bitmap;
4+
import android.graphics.Bitmap.Config;
5+
import com.google.android.gms.tasks.Task;
6+
import com.google.android.gms.tasks.Tasks;
7+
8+
/** A class that takes screenshots of the host app. */
9+
class ScreenshotTaker {
10+
11+
private static final Bitmap TEMP_FIXED_BITMAP =
12+
Bitmap.createBitmap(400, 400, Config.RGB_565);
13+
14+
/** Take a screenshot of the running host app. */
15+
Task<Bitmap> takeScreenshot() {
16+
// TODO(lkellogg): Actually take a screenshot
17+
return Tasks.forResult(TEMP_FIXED_BITMAP);
18+
}
19+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
3+
xmlns:app="http://schemas.android.com/apk/res-auto"
4+
xmlns:tools="http://schemas.android.com/tools"
5+
android:layout_width="match_parent"
6+
android:layout_height="match_parent"
7+
tools:context=".FeedbackActivity">
8+
9+
<TextView
10+
android:id="@+id/title"
11+
android:layout_width="wrap_content"
12+
android:layout_height="wrap_content"
13+
android:layout_marginTop="48dp"
14+
android:text="Enter feedback:"
15+
android:textSize="24sp"
16+
app:layout_constraintEnd_toEndOf="parent"
17+
app:layout_constraintHorizontal_bias="0.498"
18+
app:layout_constraintStart_toStartOf="parent"
19+
app:layout_constraintTop_toTopOf="parent" />
20+
<EditText
21+
android:id="@+id/feedbackText"
22+
android:layout_width="wrap_content"
23+
android:layout_height="wrap_content"
24+
android:ems="10"
25+
android:gravity="start|top"
26+
android:inputType="textMultiLine"
27+
app:layout_constraintBottom_toBottomOf="parent"
28+
app:layout_constraintEnd_toEndOf="parent"
29+
app:layout_constraintStart_toStartOf="parent"
30+
app:layout_constraintTop_toTopOf="parent" />
31+
<Button
32+
android:id="@+id/submitButton"
33+
android:layout_width="wrap_content"
34+
android:layout_height="wrap_content"
35+
android:layout_marginTop="32dp"
36+
android:text="Submit"
37+
android:onClick="submitFeedback"
38+
app:layout_constraintEnd_toEndOf="parent"
39+
app:layout_constraintHorizontal_bias="0.498"
40+
app:layout_constraintStart_toStartOf="parent"
41+
app:layout_constraintTop_toBottomOf="@+id/feedbackText" />
42+
<TextView
43+
android:id="@+id/loadingLabel"
44+
android:layout_width="wrap_content"
45+
android:layout_height="wrap_content"
46+
android:layout_marginTop="44dp"
47+
android:text="Submitting feedback..."
48+
android:visibility="invisible"
49+
app:layout_constraintEnd_toEndOf="parent"
50+
app:layout_constraintStart_toStartOf="parent"
51+
app:layout_constraintTop_toBottomOf="@+id/feedbackText" />
52+
</androidx.constraintlayout.widget.ConstraintLayout>

0 commit comments

Comments
 (0)