Skip to content

Cancel feedback notification if app is in background. #4571

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 2 commits into from
Jan 18, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
import dagger.Component;
import dagger.Module;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
import javax.inject.Singleton;

@Component(modules = AppDistroComponent.MainModule.class)
Expand Down Expand Up @@ -63,13 +65,13 @@ interface Builder {
Builder setFis(Provider<FirebaseInstallationsApi> fis);

@BindsInstance
Builder setBackgroundExecutor(@Background Executor executor);
Builder setBackgroundExecutor(@Background ScheduledExecutorService executor);

@BindsInstance
Builder setBlockingExecutor(@Blocking Executor executor);
Builder setBlockingExecutor(@Blocking ScheduledExecutorService executor);

@BindsInstance
Builder setLightweightExecutor(@Lightweight Executor executor);
Builder setLightweightExecutor(@Lightweight ScheduledExecutorService executor);

@BindsInstance
Builder setUiThreadExecutor(@UiThread Executor executor);
Expand All @@ -86,5 +88,29 @@ interface Builder {
interface MainModule {
@Binds
FirebaseAppDistribution bindAppDistro(FirebaseAppDistributionImpl impl);

@Binds
@Background
ExecutorService bindBackgroundExecutorService(@Background ScheduledExecutorService ses);

@Binds
@Background
Executor bindBackgroundExecutor(@Background ExecutorService es);

@Binds
@Lightweight
ExecutorService bindLightweightExecutorService(@Lightweight ScheduledExecutorService ses);

@Binds
@Lightweight
Executor bindLightweightExecutor(@Lightweight ExecutorService es);

@Binds
@Blocking
ExecutorService bindBlockingExecutorService(@Blocking ScheduledExecutorService ses);

@Binds
@Blocking
Executor bindBlockingExecutor(@Blocking ExecutorService es);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@

package com.google.firebase.appdistribution.impl;

import static com.google.firebase.appdistribution.impl.FirebaseAppDistributionNotificationsManager.NotificationType.APP_UPDATE;
import static com.google.firebase.appdistribution.impl.FirebaseAppDistributionNotificationsManager.NotificationType.FEEDBACK;
import static java.util.concurrent.TimeUnit.SECONDS;

import android.app.Activity;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationChannelGroup;
import android.app.PendingIntent;
Expand All @@ -28,10 +34,21 @@
import androidx.annotation.VisibleForTesting;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import com.google.firebase.annotations.concurrent.Lightweight;
import com.google.firebase.annotations.concurrent.UiThread;
import com.google.firebase.appdistribution.InterruptionLevel;
import com.google.firebase.appdistribution.impl.FirebaseAppDistributionLifecycleNotifier.OnActivityPausedListener;
import com.google.firebase.appdistribution.impl.FirebaseAppDistributionLifecycleNotifier.OnActivityResumedListener;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import javax.inject.Inject;
import javax.inject.Singleton;

@Singleton
class FirebaseAppDistributionNotificationsManager
implements OnActivityPausedListener, OnActivityResumedListener {

class FirebaseAppDistributionNotificationsManager {
private static final String TAG = "NotificationsManager";

private static final String PACKAGE_PREFIX = "com.google.firebase.appdistribution";
Expand All @@ -40,15 +57,15 @@ class FirebaseAppDistributionNotificationsManager {
static final String CHANNEL_GROUP_ID = prependPackage("notification_channel_group_id");

@VisibleForTesting
enum Notification {
enum NotificationType {
APP_UPDATE("notification_channel_id", "app_update_notification_tag"),
FEEDBACK("feedback_notification_channel_id", "feedback_notification_tag");

final String channelId;
final String tag;
final int id;

Notification(String channelId, String tag) {
NotificationType(String channelId, String tag) {
this.channelId = prependPackage(channelId);
this.tag = prependPackage(tag);
this.id = ordinal();
Expand All @@ -58,12 +75,26 @@ enum Notification {
private final Context context;
private final AppIconSource appIconSource;
private final NotificationManagerCompat notificationManager;
@Lightweight private final ScheduledExecutorService scheduledExecutorService;
@UiThread private final Executor uiThreadExecutor;

private Notification feedbackNotificationToBeShown;
private ScheduledFuture<?> feedbackNotificationCancellationFuture;

@Inject
FirebaseAppDistributionNotificationsManager(Context context, AppIconSource appIconSource) {
FirebaseAppDistributionNotificationsManager(
Context context,
AppIconSource appIconSource,
FirebaseAppDistributionLifecycleNotifier lifecycleNotifier,
@Lightweight ScheduledExecutorService scheduledExecutorService,
@UiThread Executor uiThreadExecutor) {
this.context = context;
this.appIconSource = appIconSource;
this.notificationManager = NotificationManagerCompat.from(context);
lifecycleNotifier.addOnActivityPausedListener(this);
lifecycleNotifier.addOnActivityResumedListener(this);
this.scheduledExecutorService = scheduledExecutorService;
this.uiThreadExecutor = uiThreadExecutor;
}

void showAppUpdateNotification(long totalBytes, long downloadedBytes, int stringResourceId) {
Expand All @@ -72,7 +103,7 @@ void showAppUpdateNotification(long totalBytes, long downloadedBytes, int string
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
LogWrapper.i(TAG, "Creating app update notification channel group");
createChannel(
Notification.APP_UPDATE,
APP_UPDATE,
R.string.app_update_notification_channel_name,
R.string.app_update_notification_channel_description,
InterruptionLevel.DEFAULT);
Expand All @@ -85,7 +116,7 @@ void showAppUpdateNotification(long totalBytes, long downloadedBytes, int string
}

NotificationCompat.Builder notificationBuilder =
new NotificationCompat.Builder(context, Notification.APP_UPDATE.channelId)
new NotificationCompat.Builder(context, APP_UPDATE.channelId)
.setOnlyAlertOnce(true)
.setSmallIcon(appIconSource.getNonAdaptiveIconOrDefault(context))
.setContentTitle(context.getString(stringResourceId))
Expand All @@ -97,8 +128,7 @@ void showAppUpdateNotification(long totalBytes, long downloadedBytes, int string
if (appLaunchIntent != null) {
notificationBuilder.setContentIntent(appLaunchIntent);
}
notificationManager.notify(
Notification.APP_UPDATE.tag, Notification.APP_UPDATE.id, notificationBuilder.build());
notificationManager.notify(APP_UPDATE.tag, APP_UPDATE.id, notificationBuilder.build());
}

@Nullable
Expand Down Expand Up @@ -128,7 +158,7 @@ public void showFeedbackNotification(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
LogWrapper.i(TAG, "Creating feedback notification channel group");
createChannel(
Notification.FEEDBACK,
FEEDBACK,
R.string.feedback_notification_channel_name,
R.string.feedback_notification_channel_description,
interruptionLevel);
Expand All @@ -139,36 +169,62 @@ public void showFeedbackNotification(
return;
}

uiThreadExecutor.execute(
() -> {
// ensure that class state is managed on same thread as lifecycle callbacks
cancelFeedbackCancellationFuture();
feedbackNotificationToBeShown = buildFeedbackNotification(infoText, interruptionLevel);
doShowFeedbackNotification();
});
}

// this must be run on the main (UI) thread
private void doShowFeedbackNotification() {
LogWrapper.i(TAG, "Showing feedback notification");
notificationManager.notify(FEEDBACK.tag, FEEDBACK.id, feedbackNotificationToBeShown);
}

private Notification buildFeedbackNotification(
@NonNull CharSequence infoText, @NonNull InterruptionLevel interruptionLevel) {
Intent intent = new Intent(context, TakeScreenshotAndStartFeedbackActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
intent.putExtra(TakeScreenshotAndStartFeedbackActivity.INFO_TEXT_EXTRA_KEY, infoText);
ApplicationInfo applicationInfo = context.getApplicationInfo();
PackageManager packageManager = context.getPackageManager();
CharSequence appLabel = packageManager.getApplicationLabel(applicationInfo);
NotificationCompat.Builder builder =
new NotificationCompat.Builder(context, Notification.FEEDBACK.channelId)
.setSmallIcon(R.drawable.ic_baseline_rate_review_24)
.setContentTitle(context.getString(R.string.feedback_notification_title))
.setContentText(context.getString(R.string.feedback_notification_text, appLabel))
.setPriority(interruptionLevel.notificationPriority)
.setOngoing(true)
.setOnlyAlertOnce(true)
.setAutoCancel(false)
.setContentIntent(getPendingIntent(intent, /* extraFlags= */ 0));
LogWrapper.i(TAG, "Showing feedback notification");
notificationManager.notify(
Notification.FEEDBACK.tag, Notification.FEEDBACK.id, builder.build());
return new NotificationCompat.Builder(context, FEEDBACK.channelId)
.setSmallIcon(R.drawable.ic_baseline_rate_review_24)
.setContentTitle(context.getString(R.string.feedback_notification_title))
.setContentText(context.getString(R.string.feedback_notification_text, appLabel))
.setPriority(interruptionLevel.notificationPriority)
.setOngoing(true)
.setOnlyAlertOnce(true)
.setAutoCancel(false)
.setContentIntent(getPendingIntent(intent, /* extraFlags= */ 0))
.build();
}

public void cancelFeedbackNotification() {
uiThreadExecutor.execute(
() -> {
// ensure that class state is managed on same thread as lifecycle callbacks
feedbackNotificationToBeShown = null;
cancelFeedbackCancellationFuture();
doCancelFeedbackNotification();
});
}

public void doCancelFeedbackNotification() {
LogWrapper.i(TAG, "Cancelling feedback notification");
NotificationManagerCompat.from(context)
.cancel(Notification.FEEDBACK.tag, Notification.FEEDBACK.id);
NotificationManagerCompat.from(context).cancel(FEEDBACK.tag, FEEDBACK.id);
}

@RequiresApi(Build.VERSION_CODES.O)
private void createChannel(
Notification notification, int name, int description, InterruptionLevel interruptionLevel) {
NotificationType notification,
int name,
int description,
InterruptionLevel interruptionLevel) {
notificationManager.createNotificationChannelGroup(
new NotificationChannelGroup(
CHANNEL_GROUP_ID, context.getString(R.string.notifications_group_name)));
Expand All @@ -182,6 +238,37 @@ private void createChannel(
notificationManager.createNotificationChannel(channel);
}

// this runs on the main (UI) thread
@Override
public void onPaused(Activity activity) {
LogWrapper.d(TAG, "Activity paused");
if (feedbackNotificationToBeShown != null) {
LogWrapper.d(TAG, "Scheduling cancelFeedbackNotification");
cancelFeedbackCancellationFuture();
feedbackNotificationCancellationFuture =
scheduledExecutorService.schedule(this::doCancelFeedbackNotification, 1, SECONDS);
}
}

// this runs on the main (UI) thread
@Override
public void onResumed(Activity activity) {
LogWrapper.d(TAG, "Activity resumed");
if (feedbackNotificationToBeShown != null) {
cancelFeedbackCancellationFuture();
doShowFeedbackNotification();
}
}

// this must be run on the main (UI) thread
private void cancelFeedbackCancellationFuture() {
if (feedbackNotificationCancellationFuture != null) {
LogWrapper.d(TAG, "Canceling feedbackNotificationCancellationFuture");
feedbackNotificationCancellationFuture.cancel(false);
feedbackNotificationCancellationFuture = null;
}
}

private static String prependPackage(String id) {
return String.format("%s.%s", PACKAGE_PREFIX, id);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;

/**
* Registers FirebaseAppDistribution and related components.
Expand All @@ -48,10 +49,12 @@ public class FirebaseAppDistributionRegistrar implements ComponentRegistrar {

@Override
public @NonNull List<Component<?>> getComponents() {
Qualified<Executor> backgroundExecutor = Qualified.qualified(Background.class, Executor.class);
Qualified<Executor> blockingExecutor = Qualified.qualified(Blocking.class, Executor.class);
Qualified<Executor> lightweightExecutor =
Qualified.qualified(Lightweight.class, Executor.class);
Qualified<ScheduledExecutorService> backgroundExecutor =
Qualified.qualified(Background.class, ScheduledExecutorService.class);
Qualified<ScheduledExecutorService> blockingExecutor =
Qualified.qualified(Blocking.class, ScheduledExecutorService.class);
Qualified<ScheduledExecutorService> lightweightExecutor =
Qualified.qualified(Lightweight.class, ScheduledExecutorService.class);
Qualified<Executor> uiThreadExecutor = Qualified.qualified(UiThread.class, Executor.class);
return Arrays.asList(
Component.builder(FirebaseAppDistribution.class)
Expand Down
Loading