Skip to content

Commit db7fe42

Browse files
committed
WIP - Add notification default trigger to SDK
1 parent 64cc2ae commit db7fe42

File tree

15 files changed

+393
-37
lines changed

15 files changed

+393
-37
lines changed

firebase-appdistribution-api/firebase-appdistribution-api.gradle

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ plugins {
1717
}
1818

1919
android {
20-
compileSdkVersion project.targetSdkVersion
20+
compileSdkVersion 33
2121

2222
defaultConfig {
2323
minSdkVersion 16
24-
targetSdkVersion project.targetSdkVersion
24+
targetSdkVersion 33
2525
multiDexEnabled true
2626
versionName version
2727
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -41,6 +41,7 @@ dependencies {
4141
implementation 'org.jetbrains:annotations:15.0'
4242
implementation project(':firebase-components')
4343
implementation project(':firebase-common')
44+
implementation 'androidx.activity:activity:1.5.1'
4445
implementation 'com.google.android.gms:play-services-tasks:18.0.1'
4546
testImplementation 'junit:junit:4.13.2'
4647
testImplementation "org.robolectric:robolectric:$robolectricVersion"

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,16 @@
1414

1515
package com.google.firebase.appdistribution;
1616

17+
import android.app.Activity;
1718
import android.app.NotificationChannel;
19+
import android.content.Context;
1820
import android.net.Uri;
21+
import android.os.Build;
22+
23+
import androidx.activity.result.ActivityResultCaller;
1924
import androidx.annotation.NonNull;
2025
import androidx.annotation.Nullable;
26+
import androidx.annotation.RequiresApi;
2127
import androidx.core.app.NotificationCompat;
2228
import com.google.android.gms.tasks.Task;
2329
import com.google.firebase.FirebaseApp;
@@ -181,6 +187,21 @@ public interface FirebaseAppDistribution {
181187
*/
182188
void startFeedback(@NonNull CharSequence infoText, @Nullable Uri screenshot);
183189

190+
/**
191+
* Requests dynamic {@code POST_NOTIFICATIONS} permissions, if they are not already granted.
192+
*
193+
* <p>This *must* be called unconditionally, as part of initialization path, typically as a field
194+
* initializer of an Activity or Fragment.This should only be called as an from {@link Activity#onCreate}.
195+
*
196+
* <p>This is only required if you want to use {@link showFeedbackNotification}, the device is
197+
* running Android 13 or above, and the app does not already manage this permission separately.
198+
*
199+
* @param activity the activity being initialized
200+
* @param <T>
201+
*/
202+
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
203+
<T extends Activity & ActivityResultCaller> void requestNotificationPermissions(@NonNull T activity);
204+
184205
/**
185206
* Displays a notification that, when tapped, will take a screenshot of the current activity, then
186207
* start a new activity to collect and submit feedback from the tester along with the screenshot.
@@ -204,6 +225,29 @@ public interface FirebaseAppDistribution {
204225
*/
205226
void showFeedbackNotification(@NonNull int infoTextResourceId, int importance);
206227

228+
/**
229+
* Displays a notification that, when tapped, will take a screenshot of the current activity, then
230+
* start a new activity to collect and submit feedback from the tester along with the screenshot.
231+
*
232+
* <p>When the notification is tapped:
233+
*
234+
* <ol>
235+
* <li>If the app is open, take a screenshot of the current activity
236+
* <li>If tester is not signed in, presents the tester with a Google Sign-in UI
237+
* <li>Starts a full screen activity for the tester to compose and submit the feedback
238+
* </ol>
239+
*
240+
* <p>On platforms O and above, the notification will be created in its own notification channel.
241+
*
242+
* @param infoText text to display to the tester before collecting feedback data (e.g. Terms and
243+
* Conditions)
244+
* @param importance the amount the user should be interrupted by notifications from the feedback
245+
* notification channel. See {@link NotificationChannel#setImportance}. On platforms below O,
246+
* the importance will be translated into a comparable notification priority (see {@link
247+
* NotificationCompat.Builder#setPriority}).
248+
*/
249+
void showFeedbackNotification(@NonNull CharSequence infoText, int importance);
250+
207251
/** Gets the singleton {@link FirebaseAppDistribution} instance. */
208252
@NonNull
209253
static FirebaseAppDistribution getInstance() {

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@
1414

1515
package com.google.firebase.appdistribution.internal;
1616

17+
import android.app.Activity;
18+
import android.content.Context;
1719
import android.net.Uri;
20+
21+
import androidx.activity.result.ActivityResultCaller;
1822
import androidx.annotation.NonNull;
1923
import androidx.annotation.Nullable;
2024
import com.google.android.gms.tasks.Task;
@@ -93,4 +97,19 @@ public void startFeedback(@NonNull int infoTextResourceId, @Nullable Uri screens
9397
public void startFeedback(@NonNull CharSequence infoText, @Nullable Uri screenshotUri) {
9498
delegate.startFeedback(infoText, screenshotUri);
9599
}
100+
101+
@Override
102+
public void showFeedbackNotification(@NonNull int infoTextResourceId, int importance) {
103+
delegate.showFeedbackNotification(infoTextResourceId, importance);
104+
}
105+
106+
@Override
107+
public void showFeedbackNotification(@NonNull CharSequence infoText, int importance) {
108+
delegate.showFeedbackNotification(infoText, importance);
109+
}
110+
111+
@Override
112+
public <T extends Activity & ActivityResultCaller> void requestNotificationPermissions(@NonNull T activity) {
113+
delegate.requestNotificationPermissions(activity);
114+
}
96115
}

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515
package com.google.firebase.appdistribution.internal;
1616

1717
import android.app.Activity;
18+
import android.content.Context;
1819
import android.net.Uri;
20+
21+
import androidx.activity.result.ActivityResultCaller;
1922
import androidx.annotation.NonNull;
2023
import androidx.annotation.Nullable;
2124
import com.google.android.gms.tasks.Continuation;
@@ -84,6 +87,15 @@ public void startFeedback(@NonNull int infoTextResourceId, @Nullable Uri screens
8487
@Override
8588
public void startFeedback(@NonNull CharSequence infoText, @Nullable Uri screenshotUri) {}
8689

90+
@Override
91+
public void showFeedbackNotification(@NonNull int infoTextResourceId, int importance) {}
92+
93+
@Override
94+
public void showFeedbackNotification(@NonNull CharSequence infoText, int importance) {}
95+
96+
@Override
97+
public <T extends Activity & ActivityResultCaller> void requestNotificationPermissions(@NonNull T activity) {}
98+
8799
private static <TResult> Task<TResult> getNotImplementedTask() {
88100
return Tasks.forException(
89101
new FirebaseAppDistributionException(

firebase-appdistribution/firebase-appdistribution.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@ firebaseLibrary {
2121
}
2222

2323
android {
24-
compileSdkVersion project.targetSdkVersion
24+
compileSdkVersion 33
2525

2626
defaultConfig {
2727
minSdkVersion 16
28-
targetSdkVersion project.targetSdkVersion
28+
targetSdkVersion 33
2929
multiDexEnabled true
3030
versionName version
3131
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

firebase-appdistribution/src/main/AndroidManifest.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@
5151
android:exported="false"
5252
android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
5353

54+
<activity
55+
android:name=".TakeScreenshotAndStartFeedbackActivity"
56+
android:exported="false" />
57+
5458
<provider
5559
android:name="com.google.firebase.appdistribution.impl.FirebaseAppDistributionFileProvider"
5660
android:authorities="${applicationId}.FirebaseAppDistributionFileProvider"

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

Lines changed: 139 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
package com.google.firebase.appdistribution.impl;
1616

17+
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
1718
import static com.google.firebase.appdistribution.FirebaseAppDistributionException.Status.AUTHENTICATION_CANCELED;
1819
import static com.google.firebase.appdistribution.FirebaseAppDistributionException.Status.AUTHENTICATION_FAILURE;
1920
import static com.google.firebase.appdistribution.FirebaseAppDistributionException.Status.HOST_ACTIVITY_INTERRUPTED;
@@ -24,15 +25,32 @@
2425
import static com.google.firebase.appdistribution.impl.TaskUtils.safeSetTaskException;
2526
import static com.google.firebase.appdistribution.impl.TaskUtils.safeSetTaskResult;
2627

28+
import android.Manifest;
2729
import android.app.Activity;
2830
import android.app.AlertDialog;
31+
import android.app.NotificationChannel;
32+
import android.app.NotificationChannelGroup;
33+
import android.app.NotificationManager;
34+
import android.app.PendingIntent;
2935
import android.content.Context;
3036
import android.content.Intent;
37+
import android.content.pm.ApplicationInfo;
38+
import android.content.pm.PackageManager;
3139
import android.net.Uri;
40+
import android.os.Build;
41+
import androidx.activity.result.ActivityResultCaller;
42+
import androidx.activity.result.ActivityResultLauncher;
43+
import androidx.activity.result.contract.ActivityResultContracts;
3244
import androidx.annotation.GuardedBy;
3345
import androidx.annotation.NonNull;
3446
import androidx.annotation.Nullable;
47+
import androidx.annotation.RequiresApi;
48+
import androidx.annotation.RequiresPermission;
3549
import androidx.annotation.VisibleForTesting;
50+
import androidx.core.app.ActivityCompat;
51+
import androidx.core.app.NotificationCompat;
52+
import androidx.core.app.NotificationManagerCompat;
53+
import androidx.core.content.ContextCompat;
3654
import com.google.android.gms.tasks.Task;
3755
import com.google.android.gms.tasks.TaskCompletionSource;
3856
import com.google.android.gms.tasks.Tasks;
@@ -55,6 +73,8 @@
5573
class FirebaseAppDistributionImpl implements FirebaseAppDistribution {
5674

5775
private static final int UNKNOWN_RELEASE_FILE_SIZE = -1;
76+
private static final String FEEDBACK_NOTIFICATION_CHANNEL_ID = "InAppFeedbackNotification";
77+
private static final int FEEDBACK_NOTIFICATION_ID = 1;
5878

5979
private final FirebaseApp firebaseApp;
6080
private final TesterSignInManager testerSignInManager;
@@ -340,7 +360,7 @@ public void startFeedback(@NonNull CharSequence infoText) {
340360

341361
@Override
342362
public void startFeedback(@NonNull int infoTextResourceId, @Nullable Uri screenshotUri) {
343-
startFeedback(firebaseApp.getApplicationContext().getText(infoTextResourceId), screenshotUri);
363+
startFeedback(getText(infoTextResourceId), screenshotUri);
344364
}
345365

346366
@Override
@@ -353,6 +373,120 @@ public void startFeedback(@NonNull CharSequence infoText, @Nullable Uri screensh
353373
doStartFeedback(infoText, screenshotUri);
354374
}
355375

376+
@Override
377+
public void showFeedbackNotification(@NonNull int infoTextResourceId, int importance) {
378+
showFeedbackNotification(getText(infoTextResourceId), importance);
379+
}
380+
381+
@Override
382+
public void showFeedbackNotification(@NonNull CharSequence infoText, int importance) {
383+
Context context = firebaseApp.getApplicationContext();
384+
385+
// Create the NotificationChannel, but only on API 26+ because
386+
// the NotificationChannel class is new and not in the support library
387+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
388+
NotificationChannel channel =
389+
new NotificationChannel(
390+
FEEDBACK_NOTIFICATION_CHANNEL_ID,
391+
context.getString(R.string.feedback_notification_channel_name),
392+
importance
393+
);
394+
channel.setDescription(context.getString(R.string.feedback_notification_channel_description));
395+
context.getSystemService(NotificationManager.class).createNotificationChannel(channel);
396+
}
397+
398+
if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS)
399+
== PERMISSION_GRANTED) {
400+
showNotification(importance);
401+
} else {
402+
LogWrapper.getInstance()
403+
.w("Not showing notification because permission has not been granted.");
404+
}
405+
}
406+
407+
@RequiresPermission(value = Manifest.permission.POST_NOTIFICATIONS)
408+
private void showNotification(int importance) {
409+
Context context = firebaseApp.getApplicationContext();
410+
Intent intent = new Intent(context, TakeScreenshotAndStartFeedbackActivity.class);
411+
intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
412+
PendingIntent pendingIntent =
413+
PendingIntent.getActivity(
414+
context,
415+
/* requestCode = */ 0,
416+
intent,
417+
PendingIntent.FLAG_IMMUTABLE);
418+
ApplicationInfo applicationInfo = context.getApplicationInfo();
419+
PackageManager packageManager = context.getPackageManager();
420+
CharSequence appLabel = packageManager.getApplicationLabel(applicationInfo);
421+
NotificationCompat.Builder builder =
422+
new NotificationCompat.Builder(context, FEEDBACK_NOTIFICATION_CHANNEL_ID)
423+
.setSmallIcon(R.mipmap.test_adaptive_icon) // TODO: update to an appropriate icon
424+
.setContentTitle(context.getString(R.string.feedback_notification_title))
425+
.setContentText(context.getString(R.string.feedback_notification_text, appLabel))
426+
.setPriority(convertImportanceToPriority(importance))
427+
.setContentIntent(pendingIntent);
428+
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
429+
LogWrapper.getInstance().i("Showing feedback notification.");
430+
notificationManager.notify(FEEDBACK_NOTIFICATION_ID, builder.build());
431+
}
432+
433+
private int convertImportanceToPriority(int importance) {
434+
switch (importance) {
435+
case NotificationManagerCompat.IMPORTANCE_MIN:
436+
return NotificationCompat.PRIORITY_MIN;
437+
case NotificationManagerCompat.IMPORTANCE_LOW:
438+
return NotificationCompat.PRIORITY_LOW;
439+
case NotificationManagerCompat.IMPORTANCE_HIGH:
440+
return NotificationCompat.PRIORITY_HIGH;
441+
case NotificationManagerCompat.IMPORTANCE_MAX:
442+
return NotificationCompat.PRIORITY_MAX;
443+
case NotificationManagerCompat.IMPORTANCE_UNSPECIFIED:
444+
case NotificationManagerCompat.IMPORTANCE_NONE:
445+
case NotificationManagerCompat.IMPORTANCE_DEFAULT:
446+
default:
447+
return NotificationCompat.PRIORITY_DEFAULT;
448+
}
449+
}
450+
451+
@Override
452+
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
453+
public <T extends Activity & ActivityResultCaller> void requestNotificationPermissions(
454+
@NonNull T activity) {
455+
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.POST_NOTIFICATIONS)
456+
== PERMISSION_GRANTED) {
457+
LogWrapper.getInstance().i("Already has permission to show notifications.");
458+
return;
459+
}
460+
461+
ActivityResultLauncher<String> launcher = activity.registerForActivityResult(
462+
new ActivityResultContracts.RequestPermission(), isGranted -> {
463+
if (!isGranted) {
464+
LogWrapper.getInstance().w("Permission to show notifications was denied.");
465+
// Ideally we would show a message indicating the impact of not enabling the permission,
466+
// but there's no way to know if they've permanently denied the permission, and we don't
467+
// want to show them a message after each time we try to post a notification.
468+
}
469+
});
470+
471+
// TODO: should we bother showing permission rationale?
472+
if (ActivityCompat.shouldShowRequestPermissionRationale(activity,
473+
Manifest.permission.POST_NOTIFICATIONS)) {
474+
LogWrapper.getInstance().i("Showing customer rationale for requesting permission.");
475+
new AlertDialog.Builder(activity)
476+
.setMessage(R.string.notification_permission_rationale)
477+
.setPositiveButton(R.string.notification_permission_yes_button, (a, b) -> {
478+
LogWrapper.getInstance().i("Launching request for permission.");
479+
launcher.launch(Manifest.permission.POST_NOTIFICATIONS);
480+
})
481+
.setNegativeButton(R.string.notification_permission_no_button,
482+
(a, b) -> LogWrapper.getInstance().i("Tester declined to enable notifications."))
483+
.show();
484+
} else {
485+
LogWrapper.getInstance().i("Launching request for permission without rationale.");
486+
launcher.launch(Manifest.permission.POST_NOTIFICATIONS);
487+
}
488+
}
489+
356490
private void doStartFeedback(CharSequence infoText, @Nullable Uri screenshotUri) {
357491
testerSignInManager
358492
.signInTester()
@@ -569,4 +703,8 @@ private boolean awaitingUpdateDialogConfirmation() {
569703
&& !showUpdateDialogTask.getTask().isComplete()
570704
&& remakeUpdateConfirmationDialog);
571705
}
706+
707+
private CharSequence getText(int resourceId) {
708+
return firebaseApp.getApplicationContext().getText(resourceId);
709+
}
572710
}

0 commit comments

Comments
 (0)