Skip to content

Implement collectAndSendFeedback() #3816

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 8 commits into from
Jun 16, 2022
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions firebase-appdistribution-api/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ package com.google.firebase.appdistribution {

public interface FirebaseAppDistribution {
method @NonNull public com.google.android.gms.tasks.Task<com.google.firebase.appdistribution.AppDistributionRelease> checkForNewRelease();
method public void collectAndSendFeedback();
method @NonNull public static com.google.firebase.appdistribution.FirebaseAppDistribution getInstance();
method public boolean isTesterSignedIn();
method @NonNull public com.google.android.gms.tasks.Task<java.lang.Void> signInTester();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ public interface FirebaseAppDistribution {
@NonNull
UpdateTask updateApp();

void collectAndSendFeedback();

/** Gets the singleton {@link FirebaseAppDistribution} instance. */
@NonNull
static FirebaseAppDistribution getInstance() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ public interface UpdateProgress {
* @returns the number of bytes downloaded, or -1 if called when updating to an AAB or if no new
* release is available.
*/
@NonNull
long getApkBytesDownloaded();

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

/** Returns the current {@link UpdateStatus} of the update. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,9 @@ public synchronized Task<AppDistributionRelease> checkForNewRelease() {
public UpdateTask updateApp() {
return delegate.updateApp();
}

@Override
public void collectAndSendFeedback() {
delegate.collectAndSendFeedback();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ public UpdateTask updateApp() {
return new NotImplementedUpdateTask();
}

@Override
public void collectAndSendFeedback() {
return;
}

private static <TResult> Task<TResult> getNotImplementedTask() {
return Tasks.forException(
new FirebaseAppDistributionException(
Expand Down
1 change: 1 addition & 0 deletions firebase-appdistribution/firebase-appdistribution.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,5 @@ dependencies {
annotationProcessor 'com.google.auto.value:auto-value:1.6.5'
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation "androidx.browser:browser:1.3.0"
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
}
6 changes: 6 additions & 0 deletions firebase-appdistribution/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
android:name="com.google.firebase.components:com.google.firebase.appdistribution.impl.FirebaseAppDistributionRegistrar"
android:value="com.google.firebase.components.ComponentRegistrar" />
</service>

<!-- The launch mode for Install Activity is singleTask to ensure that after the unknown sources UI
or the installation flow is complete, the Install Activity does not get recreated which causes loss of state
See here for more info - https://developer.android.com/guide/components/activities/tasks-and-back-stack#ManifestForTasks -->
Expand All @@ -45,6 +46,11 @@
</intent-filter>
</activity>

<activity
android:name=".FeedbackActivity"
android:exported="false"
android:theme="@style/Theme.AppCompat.Light.NoActionBar" />

<provider
android:name="com.google.firebase.appdistribution.impl.FirebaseAppDistributionFileProvider"
android:authorities="${applicationId}.FirebaseAppDistributionFileProvider"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class ErrorMessages {
"Failed to authenticate the tester. The tester was either not signed in, or something went wrong. Try signing in again.";

static final String AUTHORIZATION_ERROR =
"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.";
"Failed to authorize the tester. The tester does not have access to this resource (or it may not exist).";

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

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

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

static final String APK_INSTALLATION_FAILED =
"The APK failed to install or installation was canceled by the tester.";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.firebase.appdistribution.impl;

import android.graphics.Bitmap;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import com.google.firebase.FirebaseApp;

/** Activity for tester to compose and submit feedback. */
public class FeedbackActivity extends AppCompatActivity {

private static final String TAG = "FeedbackActivity";

public static final String RELEASE_NAME_EXTRA_KEY =
"com.google.firebase.appdistribution.FeedbackActivity.RELEASE_NAME";
public static final String SCREENSHOT_EXTRA_KEY =
"com.google.firebase.appdistribution.FeedbackActivity.SCREENSHOT";

private FeedbackSender feedbackSender;
private String releaseName;
private Bitmap screenshot;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
releaseName = getIntent().getStringExtra(RELEASE_NAME_EXTRA_KEY);
screenshot = getIntent().getParcelableExtra(SCREENSHOT_EXTRA_KEY);
feedbackSender = FirebaseApp.getInstance().get(FeedbackSender.class);
setContentView(R.layout.activity_feedback);
}

public void submitFeedback(View view) {
setSubmittingStateEnabled(true);
EditText feedbackText = (EditText) findViewById(R.id.feedbackText);
feedbackSender
.sendFeedback(releaseName, feedbackText.getText().toString(), screenshot)
.addOnSuccessListener(
unused -> {
LogWrapper.getInstance().i(TAG, "Feedback submitted");
Toast.makeText(this, "Feedback submitted", Toast.LENGTH_LONG).show();
finish();
})
.addOnFailureListener(
e -> {
LogWrapper.getInstance().e(TAG, "Failed to submit feedback", e);
Toast.makeText(this, "Error submitting feedback", Toast.LENGTH_LONG).show();
setSubmittingStateEnabled(false);
});
}

public void setSubmittingStateEnabled(boolean loading) {
findViewById(R.id.submitButton).setVisibility(loading ? View.INVISIBLE : View.VISIBLE);
findViewById(R.id.loadingLabel).setVisibility(loading ? View.VISIBLE : View.INVISIBLE);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.firebase.appdistribution.impl;

import android.graphics.Bitmap;
import com.google.android.gms.tasks.Task;

/** Sends tester feedback to the Tester API. */
class FeedbackSender {

private final FirebaseAppDistributionTesterApiClient testerApiClient;

FeedbackSender(FirebaseAppDistributionTesterApiClient testerApiClient) {
this.testerApiClient = testerApiClient;
}

/** Send feedback text and screenshot to the Tester API for the given release. */
Task<Void> sendFeedback(String releaseName, String feedbackText, Bitmap screenshot) {
return testerApiClient
.createFeedback(releaseName, feedbackText)
.onSuccessTask(feedbackName -> testerApiClient.attachScreenshot(feedbackName, screenshot))
.onSuccessTask(testerApiClient::commitFeedback);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,16 @@
import static com.google.firebase.appdistribution.FirebaseAppDistributionException.Status.AUTHENTICATION_FAILURE;
import static com.google.firebase.appdistribution.FirebaseAppDistributionException.Status.HOST_ACTIVITY_INTERRUPTED;
import static com.google.firebase.appdistribution.FirebaseAppDistributionException.Status.UPDATE_NOT_AVAILABLE;
import static com.google.firebase.appdistribution.impl.FeedbackActivity.RELEASE_NAME_EXTRA_KEY;
import static com.google.firebase.appdistribution.impl.FeedbackActivity.SCREENSHOT_EXTRA_KEY;
import static com.google.firebase.appdistribution.impl.TaskUtils.safeSetTaskException;
import static com.google.firebase.appdistribution.impl.TaskUtils.safeSetTaskResult;

import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
Expand All @@ -40,6 +44,8 @@
import com.google.firebase.appdistribution.UpdateProgress;
import com.google.firebase.appdistribution.UpdateStatus;
import com.google.firebase.appdistribution.UpdateTask;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

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

private final Object updateIfNewReleaseTaskLock = new Object();

Expand Down Expand Up @@ -88,7 +95,8 @@ class FirebaseAppDistributionImpl implements FirebaseAppDistribution {
@NonNull AabUpdater aabUpdater,
@NonNull SignInStorage signInStorage,
@NonNull FirebaseAppDistributionLifecycleNotifier lifecycleNotifier,
@NonNull ReleaseIdentifier releaseIdentifier) {
@NonNull ReleaseIdentifier releaseIdentifier,
@NonNull ScreenshotTaker screenshotTaker) {
this.firebaseApp = firebaseApp;
this.testerSignInManager = testerSignInManager;
this.newReleaseFetcher = newReleaseFetcher;
Expand All @@ -97,6 +105,7 @@ class FirebaseAppDistributionImpl implements FirebaseAppDistribution {
this.signInStorage = signInStorage;
this.releaseIdentifier = releaseIdentifier;
this.lifecycleNotifier = lifecycleNotifier;
this.screenshotTaker = screenshotTaker;
lifecycleNotifier.addOnActivityDestroyedListener(this::onActivityDestroyed);
lifecycleNotifier.addOnActivityPausedListener(this::onActivityPaused);
lifecycleNotifier.addOnActivityResumedListener(this::onActivityResumed);
Expand Down Expand Up @@ -297,6 +306,43 @@ private UpdateTask updateApp(boolean showDownloadInNotificationManager) {
}
}

@Override
public void collectAndSendFeedback() {
collectAndSendFeedback(Executors.newSingleThreadExecutor());
}

@VisibleForTesting
public void collectAndSendFeedback(Executor taskExecutor) {
screenshotTaker
.takeScreenshot()
.onSuccessTask(
taskExecutor,
screenshot ->
testerSignInManager
.signInTester()
.addOnFailureListener(
taskExecutor,
e ->
LogWrapper.getInstance()
.e("Failed to sign in tester. Could not collect feedback.", e))
.onSuccessTask(taskExecutor, unused -> releaseIdentifier.identifyRelease())
.onSuccessTask(
taskExecutor,
releaseName -> launchFeedbackActivity(releaseName, screenshot)))
.addOnFailureListener(
taskExecutor, e -> LogWrapper.getInstance().e("Failed to launch feedback flow", e));
}

private Task<Void> launchFeedbackActivity(String releaseName, Bitmap screenshot) {
return lifecycleNotifier.applyToForegroundActivity(
activity -> {
Intent intent = new Intent(activity, FeedbackActivity.class);
intent.putExtra(RELEASE_NAME_EXTRA_KEY, releaseName);
intent.putExtra(SCREENSHOT_EXTRA_KEY, screenshot);
activity.startActivity(intent);
});
}

@VisibleForTesting
void onActivityResumed(Activity activity) {
if (awaitingSignInDialogConfirmation()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,23 +46,39 @@ public class FirebaseAppDistributionRegistrar implements ComponentRegistrar {
Component.builder(FirebaseAppDistribution.class)
.add(Dependency.required(FirebaseApp.class))
.add(Dependency.requiredProvider(FirebaseInstallationsApi.class))
.add(Dependency.required(FeedbackSender.class))
Copy link
Contributor

Choose a reason for hiding this comment

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

Is component registry intended as a general purpose dependency injection framework?
If so, why use it just for FeedbackSender and not for things like FirebaseAppDistributionTesterApiClient, ReleaseIdentifier, etc.?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's not. In this case the FeedbackActivity needs to get an instance of the class, which depends on a Provider and thus needs to be instantiated by the Firebase Components framework.

Not sure if this is the best way to do this though. I'll reach out to the ACore folks.

Copy link
Member

Choose a reason for hiding this comment

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

why does FirebaseAppDistribution actually depend on FeedbackSender? I don't see it used inside buildFirebaseAppDistribution

Copy link
Contributor Author

@lfkellogg lfkellogg Jun 15, 2022

Choose a reason for hiding this comment

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

I was thinking that this captures the dependency that FeedbackActivity (launched by FirebaseAppDistribution) has on FeedbackSender... (EDIT: thinking more now I guess that doesn't make sense, disregard)

.factory(this::buildFirebaseAppDistribution)
// construct FirebaseAppDistribution instance on startup so we can register for
// activity lifecycle callbacks before the API is called
.alwaysEager()
.build(),
Component.builder(FeedbackSender.class)
.add(Dependency.required(FirebaseApp.class))
.add(Dependency.requiredProvider(FirebaseInstallationsApi.class))
.factory(this::buildFeedbackSender)
.build(),
LibraryVersionComponent.create("fire-appdistribution", BuildConfig.VERSION_NAME));
}

private FeedbackSender buildFeedbackSender(ComponentContainer container) {
FirebaseApp firebaseApp = container.get(FirebaseApp.class);
Provider<FirebaseInstallationsApi> firebaseInstallationsApiProvider =
container.getProvider(FirebaseInstallationsApi.class);
FirebaseAppDistributionTesterApiClient testerApiClient =
new FirebaseAppDistributionTesterApiClient(
firebaseApp, firebaseInstallationsApiProvider, new TesterApiHttpClient(firebaseApp));
return new FeedbackSender(testerApiClient);
}

private FirebaseAppDistribution buildFirebaseAppDistribution(ComponentContainer container) {
FirebaseApp firebaseApp = container.get(FirebaseApp.class);
Context context = firebaseApp.getApplicationContext();
Provider<FirebaseInstallationsApi> firebaseInstallationsApiProvider =
container.getProvider(FirebaseInstallationsApi.class);
SignInStorage signInStorage = new SignInStorage(context);
FirebaseAppDistributionTesterApiClient testerApiClient =
new FirebaseAppDistributionTesterApiClient(
firebaseApp, firebaseInstallationsApiProvider, new TesterApiHttpClient(firebaseApp));
SignInStorage signInStorage = new SignInStorage(context);
FirebaseAppDistributionLifecycleNotifier lifecycleNotifier =
FirebaseAppDistributionLifecycleNotifier.getInstance();
ReleaseIdentifier releaseIdentifier = new ReleaseIdentifier(firebaseApp, testerApiClient);
Expand All @@ -76,7 +92,8 @@ private FirebaseAppDistribution buildFirebaseAppDistribution(ComponentContainer
new AabUpdater(),
signInStorage,
lifecycleNotifier,
releaseIdentifier);
releaseIdentifier,
new ScreenshotTaker());

if (context instanceof Application) {
Application firebaseApplication = (Application) context;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

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

import android.content.Context;
import android.content.pm.PackageInfo;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
Expand Down Expand Up @@ -58,8 +57,6 @@ class ReleaseIdentifier {

/** Identify the currently installed release, returning the release name. */
Task<String> identifyRelease() {
Context context = firebaseApp.getApplicationContext();

// Attempt to find release using IAS artifact ID, which identifies app bundle releases
String iasArtifactId = null;
try {
Expand Down
Loading