Skip to content

Commit ae8db00

Browse files
committed
Add notification default trigger to in-app feedback
1 parent 765f9c8 commit ae8db00

File tree

19 files changed

+614
-115
lines changed

19 files changed

+614
-115
lines changed

firebase-appdistribution-api/api.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ package com.google.firebase.appdistribution {
1616
public interface FirebaseAppDistribution {
1717
method @NonNull public com.google.android.gms.tasks.Task<com.google.firebase.appdistribution.AppDistributionRelease> checkForNewRelease();
1818
method @NonNull public static com.google.firebase.appdistribution.FirebaseAppDistribution getInstance();
19+
method public void cancelFeedbackNotification();
1920
method public boolean isTesterSignedIn();
21+
method public void showFeedbackNotification(@NonNull int, int);
22+
method public void showFeedbackNotification(@NonNull CharSequence, int);
2023
method @NonNull public com.google.android.gms.tasks.Task<java.lang.Void> signInTester();
2124
method public void signOutTester();
2225
method public void startFeedback(int);

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

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,12 @@
1414

1515
package com.google.firebase.appdistribution;
1616

17+
import android.app.Activity;
18+
import android.app.NotificationChannel;
1719
import android.net.Uri;
1820
import androidx.annotation.NonNull;
1921
import androidx.annotation.Nullable;
22+
import androidx.core.app.NotificationCompat;
2023
import com.google.android.gms.tasks.Task;
2124
import com.google.firebase.FirebaseApp;
2225
import com.google.firebase.appdistribution.internal.FirebaseAppDistributionProxy;
@@ -159,7 +162,7 @@ public interface FirebaseAppDistribution {
159162
* @param screenshot URI to a bitmap containing a screenshot that will be included with the
160163
* report, or null to not include a screenshot
161164
*/
162-
void startFeedback(@NonNull int infoTextResourceId, @Nullable Uri screenshot);
165+
void startFeedback(int infoTextResourceId, @Nullable Uri screenshot);
163166

164167
/**
165168
* Starts an activity to collect and submit feedback from the tester, along with the given
@@ -179,6 +182,76 @@ public interface FirebaseAppDistribution {
179182
*/
180183
void startFeedback(@NonNull CharSequence infoText, @Nullable Uri screenshot);
181184

185+
/**
186+
* Displays a notification that, when tapped, will take a screenshot of the current activity, then
187+
* start a new activity to collect and submit feedback from the tester along with the screenshot.
188+
*
189+
* <p>On Android 13 and above, this method requires the <a
190+
* href="https://developer.android.com/develop/ui/views/notifications/notification-permission">runtime
191+
* permission for sending notifications</a>: {@code POST_NOTIFICATIONS}. If your app targets
192+
* Android 13 (API level 33) or above, you should <a
193+
* href="https://developer.android.com/training/permissions/requesting">request the
194+
* permission</a>.
195+
*
196+
* <p>When the notification is tapped:
197+
*
198+
* <ol>
199+
* <li>If the app is open, take a screenshot of the current activity
200+
* <li>If tester is not signed in, presents the tester with a Google Sign-in UI
201+
* <li>Starts a full screen activity for the tester to compose and submit the feedback
202+
* </ol>
203+
*
204+
* <p>On Android 8 and above, the notification will be created in its own notification channel.
205+
*
206+
* @param infoTextResourceId string resource ID of text to display to the tester before collecting
207+
* feedback data (e.g. Terms and Conditions)
208+
* @param importance the amount the user should be interrupted by notifications from the feedback
209+
* notification channel. Once the channel's importance is set it cannot be changed except by
210+
* the user. See {@link NotificationChannel#setImportance}. On platforms below Android 8, the
211+
* importance will be translated into a comparable notification priority (see {@link
212+
* NotificationCompat.Builder#setPriority}).
213+
*/
214+
void showFeedbackNotification(int infoTextResourceId, int importance);
215+
216+
/**
217+
* Displays a notification that, when tapped, will take a screenshot of the current activity, then
218+
* start a new activity to collect and submit feedback from the tester along with the screenshot.
219+
*
220+
* <p>On Android 13 and above, this method requires the <a
221+
* href="https://developer.android.com/develop/ui/views/notifications/notification-permission">runtime
222+
* permission for sending notifications</a>: {@code POST_NOTIFICATIONS}. If your app targets
223+
* Android 13 (API level 33) or above, you should <a
224+
* href="https://developer.android.com/training/permissions/requesting">request the
225+
* permission</a>.
226+
*
227+
* <p>When the notification is tapped:
228+
*
229+
* <ol>
230+
* <li>If the app is open, take a screenshot of the current activity
231+
* <li>If tester is not signed in, presents the tester with a Google Sign-in UI
232+
* <li>Starts a full screen activity for the tester to compose and submit the feedback
233+
* </ol>
234+
*
235+
* <p>On Android 8 and above, the notification will be created in its own notification channel.
236+
*
237+
* @param infoText text to display to the tester before collecting feedback data (e.g. Terms and
238+
* Conditions)
239+
* @param importance the amount the user should be interrupted by notifications from the feedback
240+
* notification channel. Once the channel's importance is set it cannot be changed except by
241+
* the user. See {@link NotificationChannel#setImportance}. On platforms below Android 8, the
242+
* importance will be translated into a comparable notification priority (see {@link
243+
* NotificationCompat.Builder#setPriority}).
244+
*/
245+
void showFeedbackNotification(@NonNull CharSequence infoText, int importance);
246+
247+
/**
248+
* Hides the notification shown with {@link #showFeedbackNotification}.
249+
*
250+
* <p>This should be called in the {@link Activity#onDestroy} of the activity that showed the
251+
* notification.
252+
*/
253+
void cancelFeedbackNotification();
254+
182255
/** Gets the singleton {@link FirebaseAppDistribution} instance. */
183256
@NonNull
184257
static FirebaseAppDistribution getInstance() {

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,27 @@ public void startFeedback(@NonNull CharSequence infoText) {
8585
}
8686

8787
@Override
88-
public void startFeedback(@NonNull int infoTextResourceId, @Nullable Uri screenshotUri) {
88+
public void startFeedback(int infoTextResourceId, @Nullable Uri screenshotUri) {
8989
delegate.startFeedback(infoTextResourceId, screenshotUri);
9090
}
9191

9292
@Override
9393
public void startFeedback(@NonNull CharSequence infoText, @Nullable Uri screenshotUri) {
9494
delegate.startFeedback(infoText, screenshotUri);
9595
}
96+
97+
@Override
98+
public void showFeedbackNotification(int infoTextResourceId, int importance) {
99+
delegate.showFeedbackNotification(infoTextResourceId, importance);
100+
}
101+
102+
@Override
103+
public void showFeedbackNotification(@NonNull CharSequence infoText, int importance) {
104+
delegate.showFeedbackNotification(infoText, importance);
105+
}
106+
107+
@Override
108+
public void cancelFeedbackNotification() {
109+
delegate.cancelFeedbackNotification();
110+
}
96111
}

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,20 @@ public void startFeedback(int infoTextResourceId) {}
7979
public void startFeedback(@NonNull CharSequence infoText) {}
8080

8181
@Override
82-
public void startFeedback(@NonNull int infoTextResourceId, @Nullable Uri screenshotUri) {}
82+
public void startFeedback(int infoTextResourceId, @Nullable Uri screenshotUri) {}
8383

8484
@Override
8585
public void startFeedback(@NonNull CharSequence infoText, @Nullable Uri screenshotUri) {}
8686

87+
@Override
88+
public void showFeedbackNotification(int infoTextResourceId, int importance) {}
89+
90+
@Override
91+
public void showFeedbackNotification(@NonNull CharSequence infoText, int importance) {}
92+
93+
@Override
94+
public void cancelFeedbackNotification() {}
95+
8796
private static <TResult> Task<TResult> getNotImplementedTask() {
8897
return Tasks.forException(
8998
new FirebaseAppDistributionException(

firebase-appdistribution/src/main/AndroidManifest.xml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@
1717
package="com.google.firebase.appdistribution.impl">
1818

1919
<uses-permission android:name="android.permission.INTERNET" />
20-
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
20+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
2121
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
22+
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
2223
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
2324

2425
<application>
@@ -51,6 +52,10 @@
5152
android:exported="false"
5253
android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
5354

55+
<activity
56+
android:name=".TakeScreenshotAndStartFeedbackActivity"
57+
android:exported="false" />
58+
5459
<provider
5560
android:name="com.google.firebase.appdistribution.impl.FirebaseAppDistributionFileProvider"
5661
android:authorities="${applicationId}.FirebaseAppDistributionFileProvider"

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ private void postUpdateProgress(
307307
.build());
308308
}
309309
if (showNotification) {
310-
appDistributionNotificationsManager.updateNotification(
310+
appDistributionNotificationsManager.showAppUpdateNotification(
311311
totalBytes, downloadedBytes, stringResourceId);
312312
}
313313
}

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

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,6 @@
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.INFO_TEXT_EXTRA_KEY;
22-
import static com.google.firebase.appdistribution.impl.FeedbackActivity.RELEASE_NAME_EXTRA_KEY;
23-
import static com.google.firebase.appdistribution.impl.FeedbackActivity.SCREENSHOT_URI_EXTRA_KEY;
2421
import static com.google.firebase.appdistribution.impl.TaskUtils.safeSetTaskException;
2522
import static com.google.firebase.appdistribution.impl.TaskUtils.safeSetTaskResult;
2623

@@ -66,6 +63,7 @@ class FirebaseAppDistributionImpl implements FirebaseAppDistribution {
6663
private final ReleaseIdentifier releaseIdentifier;
6764
private final ScreenshotTaker screenshotTaker;
6865
private final Executor taskExecutor;
66+
private final FirebaseAppDistributionNotificationsManager notificationsManager;
6967

7068
private final Object updateIfNewReleaseTaskLock = new Object();
7169

@@ -81,13 +79,11 @@ class FirebaseAppDistributionImpl implements FirebaseAppDistribution {
8179
private AlertDialog updateConfirmationDialog;
8280
private AlertDialog signInConfirmationDialog;
8381
@Nullable private Activity dialogHostActivity = null;
84-
8582
private boolean remakeSignInConfirmationDialog = false;
8683
private boolean remakeUpdateConfirmationDialog = false;
87-
8884
private TaskCompletionSource<Void> showSignInDialogTask = null;
8985
private TaskCompletionSource<Void> showUpdateDialogTask = null;
90-
private AtomicBoolean feedbackInProgress = new AtomicBoolean(false);
86+
private final AtomicBoolean feedbackInProgress = new AtomicBoolean(false);
9187

9288
@VisibleForTesting
9389
FirebaseAppDistributionImpl(
@@ -111,6 +107,8 @@ class FirebaseAppDistributionImpl implements FirebaseAppDistribution {
111107
this.lifecycleNotifier = lifecycleNotifier;
112108
this.screenshotTaker = screenshotTaker;
113109
this.taskExecutor = taskExecutor;
110+
this.notificationsManager =
111+
new FirebaseAppDistributionNotificationsManager(firebaseApp.getApplicationContext());
114112
lifecycleNotifier.addOnActivityDestroyedListener(this::onActivityDestroyed);
115113
lifecycleNotifier.addOnActivityPausedListener(this::onActivityPaused);
116114
lifecycleNotifier.addOnActivityResumedListener(this::onActivityResumed);
@@ -339,8 +337,8 @@ public void startFeedback(@NonNull CharSequence infoText) {
339337
}
340338

341339
@Override
342-
public void startFeedback(@NonNull int infoTextResourceId, @Nullable Uri screenshotUri) {
343-
startFeedback(firebaseApp.getApplicationContext().getText(infoTextResourceId), screenshotUri);
340+
public void startFeedback(int infoTextResourceId, @Nullable Uri screenshotUri) {
341+
startFeedback(getText(infoTextResourceId), screenshotUri);
344342
}
345343

346344
@Override
@@ -353,6 +351,21 @@ public void startFeedback(@NonNull CharSequence infoText, @Nullable Uri screensh
353351
doStartFeedback(infoText, screenshotUri);
354352
}
355353

354+
@Override
355+
public void showFeedbackNotification(int infoTextResourceId, int importance) {
356+
showFeedbackNotification(getText(infoTextResourceId), importance);
357+
}
358+
359+
@Override
360+
public void showFeedbackNotification(@NonNull CharSequence infoText, int importance) {
361+
notificationsManager.showFeedbackNotification(infoText, importance);
362+
}
363+
364+
@Override
365+
public void cancelFeedbackNotification() {
366+
notificationsManager.cancelFeedbackNotification();
367+
}
368+
356369
private void doStartFeedback(CharSequence infoText, @Nullable Uri screenshotUri) {
357370
testerSignInManager
358371
.signInTester()
@@ -378,10 +391,10 @@ private Task<Void> launchFeedbackActivity(
378391
activity -> {
379392
LogWrapper.getInstance().i("Launching feedback activity");
380393
Intent intent = new Intent(activity, FeedbackActivity.class);
381-
intent.putExtra(RELEASE_NAME_EXTRA_KEY, releaseName);
382-
intent.putExtra(INFO_TEXT_EXTRA_KEY, infoText);
394+
intent.putExtra(FeedbackActivity.RELEASE_NAME_EXTRA_KEY, releaseName);
395+
intent.putExtra(FeedbackActivity.INFO_TEXT_EXTRA_KEY, infoText);
383396
if (screenshotUri != null) {
384-
intent.putExtra(SCREENSHOT_URI_EXTRA_KEY, screenshotUri.toString());
397+
intent.putExtra(FeedbackActivity.SCREENSHOT_URI_EXTRA_KEY, screenshotUri.toString());
385398
}
386399
activity.startActivity(intent);
387400
});
@@ -569,4 +582,8 @@ private boolean awaitingUpdateDialogConfirmation() {
569582
&& !showUpdateDialogTask.getTask().isComplete()
570583
&& remakeUpdateConfirmationDialog);
571584
}
585+
586+
private CharSequence getText(int resourceId) {
587+
return firebaseApp.getApplicationContext().getText(resourceId);
588+
}
572589
}

0 commit comments

Comments
 (0)