From 9f7b81363f36235fd34e8d587f591b52dab8f379 Mon Sep 17 00:00:00 2001 From: Isabel Martin Date: Tue, 29 Oct 2024 09:15:24 -0700 Subject: [PATCH] MBL-1769: Push Notifications class migrated to Kotlin + migrated to RXJava2 (#2155) --- .../ui/activities/InternalToolsActivity.kt | 4 +- .../com/kickstarter/ApplicationModule.java | 2 +- .../kickstarter/libs/PushNotifications.java | 515 ---------------- .../com/kickstarter/libs/PushNotifications.kt | 579 ++++++++++++++++++ .../kickstarter/libs/PushNotificationsTest.kt | 2 +- 5 files changed, 583 insertions(+), 519 deletions(-) delete mode 100644 app/src/main/java/com/kickstarter/libs/PushNotifications.java create mode 100644 app/src/main/java/com/kickstarter/libs/PushNotifications.kt diff --git a/app/src/internal/java/com/kickstarter/ui/activities/InternalToolsActivity.kt b/app/src/internal/java/com/kickstarter/ui/activities/InternalToolsActivity.kt index 12a6d78fe1..acbdcbf3ae 100644 --- a/app/src/internal/java/com/kickstarter/ui/activities/InternalToolsActivity.kt +++ b/app/src/internal/java/com/kickstarter/ui/activities/InternalToolsActivity.kt @@ -11,8 +11,8 @@ import android.os.Bundle import android.view.View import android.webkit.URLUtil import android.widget.EditText -import androidx.activity.ComponentActivity import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import androidx.work.BackoffPolicy import androidx.work.ExistingWorkPolicy @@ -36,7 +36,7 @@ import org.joda.time.format.DateTimeFormat import java.util.concurrent.TimeUnit import javax.inject.Inject -class InternalToolsActivity : ComponentActivity() { +class InternalToolsActivity : AppCompatActivity() { @JvmField @Inject @ApiEndpointPreference diff --git a/app/src/main/java/com/kickstarter/ApplicationModule.java b/app/src/main/java/com/kickstarter/ApplicationModule.java index e465250092..887dfc6151 100644 --- a/app/src/main/java/com/kickstarter/ApplicationModule.java +++ b/app/src/main/java/com/kickstarter/ApplicationModule.java @@ -601,7 +601,7 @@ static Logout provideLogout(final @NonNull CookieManager cookieManager, final @N @Singleton @NonNull static PushNotifications providePushNotifications(final @ApplicationContext @NonNull Context context, - final @NonNull ApiClientType client) { + final @NonNull ApiClientTypeV2 client) { return new PushNotifications(context, client); } diff --git a/app/src/main/java/com/kickstarter/libs/PushNotifications.java b/app/src/main/java/com/kickstarter/libs/PushNotifications.java deleted file mode 100644 index 5e7026c1a2..0000000000 --- a/app/src/main/java/com/kickstarter/libs/PushNotifications.java +++ /dev/null @@ -1,515 +0,0 @@ -package com.kickstarter.libs; - -import android.annotation.TargetApi; -import android.app.Notification; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.os.Build; -import android.util.Pair; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -import androidx.core.app.NotificationCompat; -import androidx.core.app.TaskStackBuilder; -import androidx.core.content.ContextCompat; - -import com.bumptech.glide.Glide; -import com.bumptech.glide.load.MultiTransformation; -import com.bumptech.glide.request.FutureTarget; -import com.bumptech.glide.request.RequestOptions; -import com.google.firebase.crashlytics.FirebaseCrashlytics; -import com.kickstarter.R; -import com.kickstarter.libs.qualifiers.ApplicationContext; -import com.kickstarter.libs.utils.extensions.AnyExtKt; -import com.kickstarter.libs.utils.extensions.IntentExtKt; -import com.kickstarter.models.MessageThread; -import com.kickstarter.models.SurveyResponse; -import com.kickstarter.models.Update; -import com.kickstarter.models.pushdata.Activity; -import com.kickstarter.models.pushdata.GCM; -import com.kickstarter.services.ApiClientType; -import com.kickstarter.services.apiresponses.MessageThreadEnvelope; -import com.kickstarter.services.apiresponses.PushNotificationEnvelope; -import com.kickstarter.ui.IntentKey; -import com.kickstarter.ui.activities.ActivityFeedActivity; -import com.kickstarter.ui.activities.MessagesActivity; -import com.kickstarter.ui.activities.ProjectPageActivity; -import com.kickstarter.ui.activities.SurveyResponseActivity; -import com.kickstarter.ui.activities.UpdateActivity; -import com.bumptech.glide.load.resource.bitmap.CenterCrop; -import com.bumptech.glide.load.resource.bitmap.RoundedCorners; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ExecutionException; - -import rx.Observable; -import rx.schedulers.Schedulers; -import rx.subjects.PublishSubject; -import rx.subscriptions.CompositeSubscription; - -import static com.kickstarter.libs.rx.transformers.Transformers.combineLatestPair; -import static com.kickstarter.libs.rx.transformers.Transformers.neverError; - -public final class PushNotifications { - private static final String CHANNEL_ERRORED_PLEDGES = "ERRORED_PLEDGES"; - private static final String CHANNEL_FOLLOWING = "FOLLOWING"; - private static final String CHANNEL_MESSAGES = "MESSAGES"; - private static final String CHANNEL_PROJECT_ACTIVITY = "PROJECT_ACTIVITY"; - private static final String CHANNEL_PROJECT_REMINDER = "PROJECT_REMINDER"; - private static final String CHANNEL_PROJECT_UPDATES = "PROJECT_UPDATES"; - private static final String CHANNEL_SURVEY = "SURVEY"; - private static final String[] NOTIFICATION_CHANNELS = {CHANNEL_ERRORED_PLEDGES, - CHANNEL_FOLLOWING, - CHANNEL_MESSAGES, - CHANNEL_PROJECT_ACTIVITY, - CHANNEL_PROJECT_REMINDER, - CHANNEL_PROJECT_UPDATES, - CHANNEL_SURVEY}; - - private final @ApplicationContext Context context; - private final ApiClientType client; - - private final PublishSubject notifications = PublishSubject.create(); - private final CompositeSubscription subscriptions = new CompositeSubscription(); - - @VisibleForTesting - public Intent messageThreadIntent; - - public PushNotifications(final @ApplicationContext @NonNull Context context, final @NonNull ApiClientType client) { - this.context = context; - this.client = client; - } - - public void initialize() { - createNotificationChannels(); - - this.subscriptions.add( - this.notifications - .onBackpressureBuffer() - .filter(PushNotificationEnvelope::isErroredPledge) - .observeOn(Schedulers.newThread()) - .subscribe(this::displayNotificationFromErroredPledge) - ); - - this.subscriptions.add( - this.notifications - .onBackpressureBuffer() - .filter(PushNotificationEnvelope::isFriendFollow) - .observeOn(Schedulers.newThread()) - .subscribe(this::displayNotificationFromFriendFollowActivity) - ); - - this.subscriptions.add( - this.notifications - .onBackpressureBuffer() - .filter(PushNotificationEnvelope::isMessage) - .flatMap(this::fetchMessageThreadWithEnvelope) - .filter(AnyExtKt::isNotNull) - .observeOn(Schedulers.newThread()) - .subscribe(envelopeAndMessageThread -> - this.displayNotificationFromMessageActivity(envelopeAndMessageThread.first, envelopeAndMessageThread.second) - ) - ); - - this.subscriptions.add( - this.notifications - .onBackpressureBuffer() - .filter(PushNotificationEnvelope::isProjectActivity) - .observeOn(Schedulers.newThread()) - .subscribe(this::displayNotificationFromProjectActivity) - ); - - this.subscriptions.add( - this.notifications - .onBackpressureBuffer() - .filter(PushNotificationEnvelope::isProjectReminder) - .observeOn(Schedulers.newThread()) - .subscribe(this::displayNotificationFromProjectReminder) - ); - - this.subscriptions.add( - this.notifications - .onBackpressureBuffer() - .filter(PushNotificationEnvelope::isProjectUpdateActivity) - .flatMap(this::fetchUpdateWithEnvelope) - .filter(AnyExtKt::isNotNull) - .observeOn(Schedulers.newThread()) - .subscribe(envelopeAndUpdate -> - this.displayNotificationFromUpdateActivity(envelopeAndUpdate.first, envelopeAndUpdate.second) - ) - ); - - this.subscriptions.add( - this.notifications - .onBackpressureBuffer() - .filter(PushNotificationEnvelope::isSurvey) - .flatMap(this::fetchSurveyResponseWithEnvelope) - .filter(AnyExtKt::isNotNull) - .observeOn(Schedulers.newThread()) - .subscribe(envelopeAndSurveyResponse -> - this.displayNotificationFromSurveyResponseActivity( - envelopeAndSurveyResponse.first, - envelopeAndSurveyResponse.second - ) - ) - ); - } - - public void add(final @NonNull PushNotificationEnvelope envelope) { - this.notifications.onNext(envelope); - } - - private void createNotificationChannels() { - // Create the NotificationChannel, but only on API 26+ because - // the NotificationChannel class is new and not in the support library - if (ApiCapabilities.canCreateNotificationChannels()) { - final List channels = getListOfNotificationChannels(); - // Register the channels with the system; you can't change the importance - // or other notification behaviors after this - final NotificationManager notificationManager = this.context.getSystemService(NotificationManager.class); - if (AnyExtKt.isNotNull(notificationManager)) { - notificationManager.createNotificationChannels(channels); - } - } - } - - @TargetApi(Build.VERSION_CODES.O) - private @NonNull List getListOfNotificationChannels() { - final List channels = new ArrayList<>(NOTIFICATION_CHANNELS.length); - channels.add(getNotificationChannel(CHANNEL_ERRORED_PLEDGES, R.string.Fix_your_payment_method, NotificationManager.IMPORTANCE_HIGH)); - channels.add(getNotificationChannel(CHANNEL_MESSAGES, R.string.Messages, NotificationManager.IMPORTANCE_DEFAULT)); - channels.add(getNotificationChannel(CHANNEL_PROJECT_ACTIVITY, R.string.Project_activity, NotificationManager.IMPORTANCE_DEFAULT)); - channels.add(getNotificationChannel(CHANNEL_PROJECT_REMINDER, R.string.Project_reminders, NotificationManager.IMPORTANCE_DEFAULT)); - channels.add(getNotificationChannel(CHANNEL_PROJECT_UPDATES, R.string.Project_updates, NotificationManager.IMPORTANCE_DEFAULT)); - final NotificationChannel followingChannel = getNotificationChannel(CHANNEL_FOLLOWING, R.string.Following, NotificationManager.IMPORTANCE_DEFAULT); - followingChannel.setDescription(this.context.getString(R.string.When_following_is_on_you_can_follow_the_acticity_of_others)); - channels.add(followingChannel); - channels.add(getNotificationChannel(CHANNEL_SURVEY, R.string.Reward_surveys, NotificationManager.IMPORTANCE_HIGH)); - return channels; - } - - @TargetApi(Build.VERSION_CODES.O) - private @NonNull NotificationChannel getNotificationChannel(final @NonNull String channelId, final int nameResId, final int importance) { - final CharSequence name = this.context.getString(nameResId); - return new NotificationChannel(channelId, name, importance); - } - - private void displayNotificationFromErroredPledge(final @NonNull PushNotificationEnvelope envelope) { - final GCM gcm = envelope.gcm(); - - final PushNotificationEnvelope.ErroredPledge erroredPledge = envelope.erroredPledge(); - if (erroredPledge == null) { - return; - } - - final Long projectId = erroredPledge.projectId(); - final Intent projectIntent = projectIntent(envelope, projectId.toString()) - .putExtra(IntentKey.EXPAND_PLEDGE_SHEET, true); - final Notification notification = notificationBuilder(gcm.title(), gcm.alert(), CHANNEL_PROJECT_REMINDER) - .setContentIntent(projectContentIntent(envelope, projectIntent)) - .build(); - - notificationManager().notify(envelope.signature(), notification); - } - - private void displayNotificationFromFriendFollowActivity(final @NonNull PushNotificationEnvelope envelope) { - final GCM gcm = envelope.gcm(); - - final Activity activity = envelope.activity(); - if (activity == null) { - return; - } - - final Notification notification = notificationBuilder(gcm.title(), gcm.alert(), CHANNEL_FOLLOWING) - .setLargeIcon(fetchBitmap(activity.userPhoto(), true)) - .setContentIntent(friendFollowActivityIntent(envelope)) - .build(); - notificationManager().notify(envelope.signature(), notification); - } - - private void displayNotificationFromMessageActivity(final @NonNull PushNotificationEnvelope envelope, - final @NonNull MessageThread messageThread) { - final GCM gcm = envelope.gcm(); - - final PushNotificationEnvelope.Message message = envelope.message(); - if (message == null) { - return; - } - - final Notification notification = notificationBuilder(gcm.title(), gcm.alert(), CHANNEL_MESSAGES) - .setContentIntent(messageThreadIntent(envelope, messageThread)) - .build(); - - notificationManager().notify(envelope.signature(), notification); - } - - private void displayNotificationFromProjectActivity(final @NonNull PushNotificationEnvelope envelope) { - final GCM gcm = envelope.gcm(); - - final Activity activity = envelope.activity(); - if (activity == null) { - return; - } - final Long projectId = activity.projectId(); - if (projectId == null) { - return; - } - final String projectPhoto = activity.projectPhoto(); - - NotificationCompat.Builder notificationBuilder = notificationBuilder(gcm.title(), gcm.alert(), CHANNEL_PROJECT_ACTIVITY) - .setContentIntent(projectContentIntent(envelope, projectIntent(envelope, projectId.toString()))); - if (projectPhoto != null) { - notificationBuilder = notificationBuilder.setLargeIcon(fetchBitmap(projectPhoto, false)); - } - final Notification notification = notificationBuilder.build(); - - notificationManager().notify(envelope.signature(), notification); - } - - private void displayNotificationFromProjectReminder(final @NonNull PushNotificationEnvelope envelope) { - final GCM gcm = envelope.gcm(); - - final PushNotificationEnvelope.Project project = envelope.project(); - if (project == null) { - return; - } - - final Intent projectIntent = projectIntent(envelope, Long.toString(project.id())); - final Notification notification = notificationBuilder(gcm.title(), gcm.alert(), CHANNEL_PROJECT_REMINDER) - .setContentIntent(projectContentIntent(envelope, projectIntent)) - .setLargeIcon(fetchBitmap(project.photo(), false)) - .build(); - - notificationManager().notify(envelope.signature(), notification); - } - - private void displayNotificationFromSurveyResponseActivity(final @NonNull PushNotificationEnvelope envelope, - final @NonNull SurveyResponse surveyResponse) { - - final GCM gcm = envelope.gcm(); - - final PushNotificationEnvelope.Survey survey = envelope.survey(); - if (survey == null) { - return; - } - - final Notification notification = notificationBuilder(gcm.title(), gcm.alert(), CHANNEL_SURVEY) - .setContentIntent(surveyResponseContentIntent(envelope, surveyResponse)) - .build(); - notificationManager().notify(envelope.signature(), notification); - } - - private void displayNotificationFromUpdateActivity(final @NonNull PushNotificationEnvelope envelope, - final @NonNull Update update) { - - final GCM gcm = envelope.gcm(); - - final Activity activity = envelope.activity(); - if (activity == null) { - return; - } - final Long updateId = activity.updateId(); - if (updateId == null) { - return; - } - final Long projectId = activity.projectId(); - if (projectId == null) { - return; - } - - final String projectParam = projectId.toString(); - - final Notification notification = notificationBuilder(gcm.title(), gcm.alert(), CHANNEL_PROJECT_UPDATES) - .setContentIntent(projectUpdateContentIntent(envelope, update, projectParam)) - .setLargeIcon(fetchBitmap(activity.projectPhoto(), false)) - .build(); - notificationManager().notify(envelope.signature(), notification); - } - - - private @NonNull PendingIntent friendFollowActivityIntent(final @NonNull PushNotificationEnvelope envelope) { - final Intent messageThreadIntent = new Intent(this.context, ActivityFeedActivity.class); - - final TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(this.context) - .addNextIntentWithParentStack(messageThreadIntent); - - return taskStackBuilder.getPendingIntent(envelope.signature(), PendingIntent.FLAG_IMMUTABLE); - } - - @VisibleForTesting - public @NonNull PendingIntent messageThreadIntent(final @NonNull PushNotificationEnvelope envelope, - final @NonNull MessageThread messageThread) { - - this.messageThreadIntent = new Intent(this.context, MessagesActivity.class) - .putExtra(IntentKey.MESSAGE_THREAD, messageThread) - .putExtra(IntentKey.MESSAGE_SCREEN_SOURCE_CONTEXT, MessagePreviousScreenType.PUSH); - - final TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(this.context) - .addNextIntentWithParentStack(this.messageThreadIntent); - - return taskStackBuilder.getPendingIntent(envelope.signature(), PendingIntent.FLAG_IMMUTABLE); - } - - private @NonNull NotificationCompat.Builder notificationBuilder(final @NonNull String title, - final @NonNull String text, final @NonNull String channelId) { - - return new NotificationCompat.Builder(this.context, channelId) - .setSmallIcon(R.drawable.ic_kickstarter_micro_k) - .setColor(ContextCompat.getColor(this.context, R.color.kds_create_700)) - .setContentText(text) - .setContentTitle(title) - .setStyle(new NotificationCompat.BigTextStyle().bigText(text)) - .setAutoCancel(true); - } - - private @NonNull PendingIntent projectContentIntent(final @NonNull PushNotificationEnvelope envelope, - final @NonNull Intent projectIntent) { - - final TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(this.context) - .addNextIntentWithParentStack(projectIntent); - - return taskStackBuilder.getPendingIntent(envelope.signature(), PendingIntent.FLAG_IMMUTABLE); - } - - private @NonNull PendingIntent projectUpdateContentIntent(final @NonNull PushNotificationEnvelope envelope, - final @NonNull Update update, final @NonNull String projectParam) { - - final Intent projectIntent = IntentExtKt.getProjectIntent(new Intent(), this.context) - .putExtra(IntentKey.PROJECT_PARAM, projectParam) - .putExtra(IntentKey.REF_TAG, RefTag.push()); - - final Intent updateIntent = new Intent(this.context, UpdateActivity.class) - .putExtra(IntentKey.PROJECT_PARAM, projectParam) - .putExtra(IntentKey.UPDATE, update) - .putExtra(IntentKey.PUSH_NOTIFICATION_ENVELOPE, envelope); - - final TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(this.context) - .addNextIntentWithParentStack(projectIntent) - .addNextIntent(updateIntent); - - return taskStackBuilder.getPendingIntent(envelope.signature(), PendingIntent.FLAG_IMMUTABLE); - } - - private @NonNull PendingIntent surveyResponseContentIntent(final @NonNull PushNotificationEnvelope envelope, - final @NonNull SurveyResponse surveyResponse) { - - final Intent activityFeedIntent = new Intent(this.context, ActivityFeedActivity.class); - - final Intent surveyResponseIntent = new Intent(this.context, SurveyResponseActivity.class) - .putExtra(IntentKey.SURVEY_RESPONSE, surveyResponse); - - final TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(this.context) - .addNextIntentWithParentStack(activityFeedIntent) - .addNextIntent(surveyResponseIntent); - - return taskStackBuilder.getPendingIntent(envelope.signature(), PendingIntent.FLAG_IMMUTABLE); - } - - private @Nullable Bitmap fetchBitmap(final @Nullable String url, final boolean transformIntoCircle) { - if (url == null) { - return null; - } - - try { - if (transformIntoCircle) { - final FutureTarget circleCrop = Glide.with(this.context) - .asBitmap() - .load(url) - .error(R.drawable.logo) - .apply(RequestOptions.circleCropTransform()) - .submit(); - return circleCrop.get(); - } else { - final FutureTarget SquareRoundCorners = Glide.with(this.context) - .asBitmap() - .load(url) - .error(R.drawable.logo) - .transform(new MultiTransformation<>(new CenterCrop(), new RoundedCorners(10))) - .submit(); - return SquareRoundCorners.get(); - } - } catch (ExecutionException | InterruptedException e) { - final Throwable error = new Throwable(url, e); - FirebaseCrashlytics.getInstance().recordException(error); - return BitmapFactory.decodeResource(this.context.getResources(), R.drawable.logo); - } - } - - private @NonNull NotificationManager notificationManager() { - return (NotificationManager) this.context.getSystemService(Context.NOTIFICATION_SERVICE); - } - - private @Nullable Observable> fetchMessageThreadWithEnvelope( - final @NonNull PushNotificationEnvelope envelope) { - - final PushNotificationEnvelope.Message message = envelope.message(); - if (message == null) { - return null; - } - - final Observable messageThread = this.client.fetchMessagesForThread(message.messageThreadId()) - .compose(neverError()) - .map(MessageThreadEnvelope::messageThread); - - return Observable.just(envelope) - .compose(combineLatestPair(messageThread)); - } - - private @Nullable Observable> fetchSurveyResponseWithEnvelope( - final @NonNull PushNotificationEnvelope envelope) { - - final PushNotificationEnvelope.Survey survey = envelope.survey(); - if (survey == null) { - return null; - } - - final Observable surveyResponse = this.client.fetchSurveyResponse(survey.id()) - .compose(neverError()); - - return Observable.just(envelope) - .compose(combineLatestPair(surveyResponse)); - } - - private @Nullable Observable> fetchUpdateWithEnvelope( - final @NonNull PushNotificationEnvelope envelope) { - - final Activity activity = envelope.activity(); - if (activity == null) { - return null; - } - - final Long updateId = activity.updateId(); - if (updateId == null) { - return null; - } - - final Long projectId = activity.projectId(); - if (projectId == null) { - return null; - } - - final String projectParam = projectId.toString(); - final String updateParam = updateId.toString(); - - final Observable update = this.client.fetchUpdate(projectParam, updateParam) - .compose(neverError()); - - return Observable.just(envelope) - .compose(combineLatestPair(update)); - } - - private @NonNull Intent projectIntent(final @NonNull PushNotificationEnvelope envelope, final @NonNull String projectParam) { - final Intent intent = new Intent(this.context, ProjectPageActivity.class); - return intent - .putExtra(IntentKey.PROJECT_PARAM, projectParam) - .putExtra(IntentKey.PUSH_NOTIFICATION_ENVELOPE, envelope) - .putExtra(IntentKey.REF_TAG, RefTag.push()); - } -} diff --git a/app/src/main/java/com/kickstarter/libs/PushNotifications.kt b/app/src/main/java/com/kickstarter/libs/PushNotifications.kt new file mode 100644 index 0000000000..f3acd51d56 --- /dev/null +++ b/app/src/main/java/com/kickstarter/libs/PushNotifications.kt @@ -0,0 +1,579 @@ +package com.kickstarter.libs + +import android.annotation.TargetApi +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.os.Build +import android.util.Pair +import androidx.annotation.VisibleForTesting +import androidx.core.app.NotificationCompat +import androidx.core.app.TaskStackBuilder +import androidx.core.content.ContextCompat +import com.bumptech.glide.Glide +import com.bumptech.glide.load.MultiTransformation +import com.bumptech.glide.load.resource.bitmap.CenterCrop +import com.bumptech.glide.load.resource.bitmap.RoundedCorners +import com.bumptech.glide.request.RequestOptions +import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.kickstarter.R +import com.kickstarter.libs.RefTag.Companion.push +import com.kickstarter.libs.qualifiers.ApplicationContext +import com.kickstarter.libs.rx.transformers.Transformers +import com.kickstarter.libs.utils.extensions.getProjectIntent +import com.kickstarter.libs.utils.extensions.isNotNull +import com.kickstarter.models.MessageThread +import com.kickstarter.models.SurveyResponse +import com.kickstarter.models.Update +import com.kickstarter.services.ApiClientTypeV2 +import com.kickstarter.services.apiresponses.MessageThreadEnvelope +import com.kickstarter.services.apiresponses.PushNotificationEnvelope +import com.kickstarter.ui.IntentKey +import com.kickstarter.ui.activities.ActivityFeedActivity +import com.kickstarter.ui.activities.MessagesActivity +import com.kickstarter.ui.activities.ProjectPageActivity +import com.kickstarter.ui.activities.SurveyResponseActivity +import com.kickstarter.ui.activities.UpdateActivity +import io.reactivex.Observable +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import io.reactivex.subjects.PublishSubject +import java.util.concurrent.ExecutionException + +class PushNotifications( + @field:ApplicationContext @param:ApplicationContext private val context: Context, + private val client: ApiClientTypeV2 +) { + private val notifications: PublishSubject = PublishSubject.create() + private val subscriptions = CompositeDisposable() + + @VisibleForTesting + var messageThreadIntent: Intent? = null + + fun initialize() { + createNotificationChannels() + + subscriptions.add( + notifications + .filter { obj: PushNotificationEnvelope -> obj.isErroredPledge() } + .observeOn(Schedulers.newThread()) + .subscribe { envelope: PushNotificationEnvelope -> + this.displayNotificationFromErroredPledge( + envelope + ) + } + ) + + subscriptions.add( + notifications + .filter { obj: PushNotificationEnvelope -> obj.isFriendFollow() } + .observeOn(Schedulers.newThread()) + .subscribe { envelope: PushNotificationEnvelope -> + this.displayNotificationFromFriendFollowActivity( + envelope + ) + } + ) + + subscriptions.add( + notifications + .filter { obj: PushNotificationEnvelope -> obj.isMessage() } + .flatMap { envelope: PushNotificationEnvelope -> + this.fetchMessageThreadWithEnvelope( + envelope + ) + } + .filter { isNotNull() } + .observeOn(Schedulers.newThread()) + .subscribe { envelopeAndMessageThread -> + this.displayNotificationFromMessageActivity( + envelopeAndMessageThread.first, envelopeAndMessageThread.second + ) + } + ) + + subscriptions.add( + notifications + .filter { obj: PushNotificationEnvelope -> obj.isProjectActivity() } + .observeOn(Schedulers.newThread()) + .subscribe { envelope: PushNotificationEnvelope -> + this.displayNotificationFromProjectActivity( + envelope + ) + } + ) + + subscriptions.add( + notifications + .filter { obj: PushNotificationEnvelope -> obj.isProjectReminder() } + .observeOn(Schedulers.newThread()) + .subscribe { envelope: PushNotificationEnvelope -> + this.displayNotificationFromProjectReminder( + envelope + ) + } + ) + + subscriptions.add( + notifications + .filter { obj: PushNotificationEnvelope -> obj.isProjectUpdateActivity() } + .flatMap { envelope: PushNotificationEnvelope -> + this.fetchUpdateWithEnvelope( + envelope + ) + } + .filter { isNotNull() } + .observeOn(Schedulers.newThread()) + .subscribe { envelopeAndUpdate: Pair -> + this.displayNotificationFromUpdateActivity( + envelopeAndUpdate.first, + envelopeAndUpdate.second + ) + } + ) + + subscriptions.add( + notifications + .filter { obj: PushNotificationEnvelope -> obj.isSurvey() } + .flatMap { envelope: PushNotificationEnvelope -> + this.fetchSurveyResponseWithEnvelope( + envelope + ) + } + .filter { + isNotNull() + } + .observeOn(Schedulers.newThread()) + .subscribe { envelopeAndSurveyResponse: Pair -> + this.displayNotificationFromSurveyResponseActivity( + envelopeAndSurveyResponse.first, + envelopeAndSurveyResponse.second + ) + } + ) + } + + fun add(envelope: PushNotificationEnvelope) { + notifications.onNext(envelope) + } + + private fun createNotificationChannels() { + // Create the NotificationChannel, but only on API 26+ because + // the NotificationChannel class is new and not in the support library + if (ApiCapabilities.canCreateNotificationChannels()) { + val channels = listOfNotificationChannels + // Register the channels with the system; you can't change the importance + // or other notification behaviors after this + val notificationManager = context.getSystemService( + NotificationManager::class.java + ) + if (notificationManager.isNotNull()) { + notificationManager.createNotificationChannels(channels) + } + } + } + + @get:TargetApi(Build.VERSION_CODES.O) + private val listOfNotificationChannels: List + get() { + val channels: MutableList = ArrayList(NOTIFICATION_CHANNELS.size) + channels.add( + getNotificationChannel( + CHANNEL_ERRORED_PLEDGES, + R.string.Fix_your_payment_method, + NotificationManager.IMPORTANCE_HIGH + ) + ) + channels.add( + getNotificationChannel( + CHANNEL_MESSAGES, + R.string.Messages, + NotificationManager.IMPORTANCE_DEFAULT + ) + ) + channels.add( + getNotificationChannel( + CHANNEL_PROJECT_ACTIVITY, + R.string.Project_activity, + NotificationManager.IMPORTANCE_DEFAULT + ) + ) + channels.add( + getNotificationChannel( + CHANNEL_PROJECT_REMINDER, + R.string.Project_reminders, + NotificationManager.IMPORTANCE_DEFAULT + ) + ) + channels.add( + getNotificationChannel( + CHANNEL_PROJECT_UPDATES, + R.string.Project_updates, + NotificationManager.IMPORTANCE_DEFAULT + ) + ) + val followingChannel = getNotificationChannel( + CHANNEL_FOLLOWING, + R.string.Following, + NotificationManager.IMPORTANCE_DEFAULT + ) + followingChannel.description = + context.getString(R.string.When_following_is_on_you_can_follow_the_acticity_of_others) + channels.add(followingChannel) + channels.add( + getNotificationChannel( + CHANNEL_SURVEY, + R.string.Reward_surveys, + NotificationManager.IMPORTANCE_HIGH + ) + ) + return channels + } + + @TargetApi(Build.VERSION_CODES.O) + private fun getNotificationChannel( + channelId: String, + nameResId: Int, + importance: Int + ): NotificationChannel { + val name: CharSequence = context.getString(nameResId) + return NotificationChannel(channelId, name, importance) + } + + private fun displayNotificationFromErroredPledge(envelope: PushNotificationEnvelope) { + val gcm = envelope.gcm() + + val erroredPledge = envelope.erroredPledge() ?: return + + val projectId = erroredPledge.projectId() + val projectIntent = projectIntent(envelope, projectId.toString()) + .putExtra(IntentKey.EXPAND_PLEDGE_SHEET, true) + val notification = notificationBuilder(gcm.title(), gcm.alert(), CHANNEL_PROJECT_REMINDER) + .setContentIntent(projectContentIntent(envelope, projectIntent)) + .build() + + notificationManager().notify(envelope.signature(), notification) + } + + private fun displayNotificationFromFriendFollowActivity(envelope: PushNotificationEnvelope) { + val gcm = envelope.gcm() + + val activity = envelope.activity() ?: return + + val notification = notificationBuilder(gcm.title(), gcm.alert(), CHANNEL_FOLLOWING) + .setLargeIcon(fetchBitmap(activity.userPhoto(), true)) + .setContentIntent(friendFollowActivityIntent(envelope)) + .build() + notificationManager().notify(envelope.signature(), notification) + } + + private fun displayNotificationFromMessageActivity( + envelope: PushNotificationEnvelope, + messageThread: MessageThread? + ) { + val gcm = envelope.gcm() + + val message = envelope.message() + + messageThread?.let { + val notification = notificationBuilder(gcm.title(), gcm.alert(), CHANNEL_MESSAGES) + .setContentIntent(messageThreadIntent(envelope, messageThread)) + .build() + + notificationManager().notify(envelope.signature(), notification) + } + } + + private fun displayNotificationFromProjectActivity(envelope: PushNotificationEnvelope) { + val gcm = envelope.gcm() + + val activity = envelope.activity() ?: return + val projectId = activity.projectId() ?: return + val projectPhoto = activity.projectPhoto() + + var notificationBuilder = + notificationBuilder(gcm.title(), gcm.alert(), CHANNEL_PROJECT_ACTIVITY) + .setContentIntent( + projectContentIntent( + envelope, + projectIntent(envelope, projectId.toString()) + ) + ) + if (projectPhoto != null) { + notificationBuilder = notificationBuilder.setLargeIcon(fetchBitmap(projectPhoto, false)) + } + val notification = notificationBuilder.build() + + notificationManager().notify(envelope.signature(), notification) + } + + private fun displayNotificationFromProjectReminder(envelope: PushNotificationEnvelope) { + val gcm = envelope.gcm() + + val project = envelope.project() ?: return + + val projectIntent = projectIntent(envelope, project.id().toString()) + val notification = notificationBuilder(gcm.title(), gcm.alert(), CHANNEL_PROJECT_REMINDER) + .setContentIntent(projectContentIntent(envelope, projectIntent)) + .setLargeIcon(fetchBitmap(project.photo(), false)) + .build() + + notificationManager().notify(envelope.signature(), notification) + } + + private fun displayNotificationFromSurveyResponseActivity( + envelope: PushNotificationEnvelope, + surveyResponse: SurveyResponse + ) { + val gcm = envelope.gcm() + + val survey = envelope.survey() ?: return + + val notification = notificationBuilder(gcm.title(), gcm.alert(), CHANNEL_SURVEY) + .setContentIntent(surveyResponseContentIntent(envelope, surveyResponse)) + .build() + notificationManager().notify(envelope.signature(), notification) + } + + private fun displayNotificationFromUpdateActivity( + envelope: PushNotificationEnvelope, + update: Update + ) { + val gcm = envelope.gcm() + + val activity = envelope.activity() ?: return + val updateId = activity.updateId() ?: return + val projectId = activity.projectId() ?: return + + val projectParam = projectId.toString() + + val notification = notificationBuilder(gcm.title(), gcm.alert(), CHANNEL_PROJECT_UPDATES) + .setContentIntent(projectUpdateContentIntent(envelope, update, projectParam)) + .setLargeIcon(fetchBitmap(activity.projectPhoto(), false)) + .build() + notificationManager().notify(envelope.signature(), notification) + } + + private fun friendFollowActivityIntent(envelope: PushNotificationEnvelope): PendingIntent { + val messageThreadIntent = Intent(this.context, ActivityFeedActivity::class.java) + + val taskStackBuilder = TaskStackBuilder.create(this.context) + .addNextIntentWithParentStack(messageThreadIntent) + + return taskStackBuilder.getPendingIntent( + envelope.signature(), + PendingIntent.FLAG_IMMUTABLE + )!! + } + + @VisibleForTesting + fun messageThreadIntent( + envelope: PushNotificationEnvelope, + messageThread: MessageThread + ): PendingIntent { + this.messageThreadIntent = Intent(this.context, MessagesActivity::class.java) + .putExtra(IntentKey.MESSAGE_THREAD, messageThread) + .putExtra(IntentKey.MESSAGE_SCREEN_SOURCE_CONTEXT, MessagePreviousScreenType.PUSH) + + val taskStackBuilder = TaskStackBuilder.create(this.context) + .addNextIntentWithParentStack(messageThreadIntent!!) + + return taskStackBuilder.getPendingIntent( + envelope.signature(), + PendingIntent.FLAG_IMMUTABLE + )!! + } + + private fun notificationBuilder( + title: String, + text: String, + channelId: String + ): NotificationCompat.Builder { + return NotificationCompat.Builder(this.context, channelId) + .setSmallIcon(R.drawable.ic_kickstarter_micro_k) + .setColor(ContextCompat.getColor(this.context, R.color.kds_create_700)) + .setContentText(text) + .setContentTitle(title) + .setStyle(NotificationCompat.BigTextStyle().bigText(text)) + .setAutoCancel(true) + } + + private fun projectContentIntent( + envelope: PushNotificationEnvelope, + projectIntent: Intent + ): PendingIntent { + val taskStackBuilder = TaskStackBuilder.create(this.context) + .addNextIntentWithParentStack(projectIntent) + + return taskStackBuilder.getPendingIntent( + envelope.signature(), + PendingIntent.FLAG_IMMUTABLE + )!! + } + + private fun projectUpdateContentIntent( + envelope: PushNotificationEnvelope, + update: Update, + projectParam: String + ): PendingIntent { + val projectIntent = Intent().getProjectIntent(this.context) + .putExtra(IntentKey.PROJECT_PARAM, projectParam) + .putExtra(IntentKey.REF_TAG, push()) + + val updateIntent = Intent(this.context, UpdateActivity::class.java) + .putExtra(IntentKey.PROJECT_PARAM, projectParam) + .putExtra(IntentKey.UPDATE, update) + .putExtra(IntentKey.PUSH_NOTIFICATION_ENVELOPE, envelope) + + val taskStackBuilder = TaskStackBuilder.create(this.context) + .addNextIntentWithParentStack(projectIntent) + .addNextIntent(updateIntent) + + return taskStackBuilder.getPendingIntent( + envelope.signature(), + PendingIntent.FLAG_IMMUTABLE + )!! + } + + private fun surveyResponseContentIntent( + envelope: PushNotificationEnvelope, + surveyResponse: SurveyResponse + ): PendingIntent { + val activityFeedIntent = Intent(this.context, ActivityFeedActivity::class.java) + + val surveyResponseIntent = Intent(this.context, SurveyResponseActivity::class.java) + .putExtra(IntentKey.SURVEY_RESPONSE, surveyResponse) + + val taskStackBuilder = TaskStackBuilder.create(this.context) + .addNextIntentWithParentStack(activityFeedIntent) + .addNextIntent(surveyResponseIntent) + + return taskStackBuilder.getPendingIntent( + envelope.signature(), + PendingIntent.FLAG_IMMUTABLE + )!! + } + + private fun fetchBitmap(url: String?, transformIntoCircle: Boolean): Bitmap? { + if (url == null) { + return null + } + + try { + if (transformIntoCircle) { + val circleCrop = Glide.with(this.context) + .asBitmap() + .load(url) + .error(R.drawable.logo) + .apply(RequestOptions.circleCropTransform()) + .submit() + return circleCrop.get() + } else { + val SquareRoundCorners = Glide.with(this.context) + .asBitmap() + .load(url) + .error(R.drawable.logo) + .transform(MultiTransformation(CenterCrop(), RoundedCorners(10))) + .submit() + return SquareRoundCorners.get() + } + } catch (e: ExecutionException) { + val error = Throwable(url, e) + FirebaseCrashlytics.getInstance().recordException(error) + return BitmapFactory.decodeResource(context.resources, R.drawable.logo) + } catch (e: InterruptedException) { + val error = Throwable(url, e) + FirebaseCrashlytics.getInstance().recordException(error) + return BitmapFactory.decodeResource(context.resources, R.drawable.logo) + } + } + + private fun notificationManager(): NotificationManager { + return context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + } + + private fun fetchMessageThreadWithEnvelope( + envelope: PushNotificationEnvelope + ): Observable>? { + val message = envelope.message()?.messageThreadId() + + val messageThread = message?.let { + client.fetchMessagesForThread(it) + .compose(Transformers.neverErrorV2()) + .map { obj: MessageThreadEnvelope -> obj.messageThread() } + } ?: Observable.empty() + + val envelopeAndThread = Observable.just(envelope) + .compose(Transformers.combineLatestPair(messageThread)) + + return envelopeAndThread + } + + private fun fetchSurveyResponseWithEnvelope( + envelope: PushNotificationEnvelope + ): Observable>? { + val survey = envelope.survey() ?: return null + + val surveyResponse = client.fetchSurveyResponse(survey.id()) + .map { + it + } + .compose(Transformers.neverErrorV2()) + + return Observable.just(envelope) + .map { + it + } + .compose(Transformers.combineLatestPair(surveyResponse)) + } + + private fun fetchUpdateWithEnvelope( + envelope: PushNotificationEnvelope + ): Observable>? { + val activity = envelope.activity() ?: return null + + val updateId = activity.updateId() ?: return null + + val projectId = activity.projectId() ?: return null + + val projectParam = projectId.toString() + val updateParam = updateId.toString() + + val update = client.fetchUpdate(projectParam, updateParam) + .compose(Transformers.neverErrorV2()) + + return Observable.just(envelope) + .compose(Transformers.combineLatestPair(update)) + } + + private fun projectIntent(envelope: PushNotificationEnvelope, projectParam: String): Intent { + val intent = Intent(this.context, ProjectPageActivity::class.java) + return intent + .putExtra(IntentKey.PROJECT_PARAM, projectParam) + .putExtra(IntentKey.PUSH_NOTIFICATION_ENVELOPE, envelope) + .putExtra(IntentKey.REF_TAG, push()) + } + + companion object { + private const val CHANNEL_ERRORED_PLEDGES = "ERRORED_PLEDGES" + private const val CHANNEL_FOLLOWING = "FOLLOWING" + private const val CHANNEL_MESSAGES = "MESSAGES" + private const val CHANNEL_PROJECT_ACTIVITY = "PROJECT_ACTIVITY" + private const val CHANNEL_PROJECT_REMINDER = "PROJECT_REMINDER" + private const val CHANNEL_PROJECT_UPDATES = "PROJECT_UPDATES" + private const val CHANNEL_SURVEY = "SURVEY" + private val NOTIFICATION_CHANNELS = arrayOf( + CHANNEL_ERRORED_PLEDGES, + CHANNEL_FOLLOWING, + CHANNEL_MESSAGES, + CHANNEL_PROJECT_ACTIVITY, + CHANNEL_PROJECT_REMINDER, + CHANNEL_PROJECT_UPDATES, + CHANNEL_SURVEY + ) + } +} diff --git a/app/src/test/java/com/kickstarter/libs/PushNotificationsTest.kt b/app/src/test/java/com/kickstarter/libs/PushNotificationsTest.kt index b8104dfe62..948d986efc 100644 --- a/app/src/test/java/com/kickstarter/libs/PushNotificationsTest.kt +++ b/app/src/test/java/com/kickstarter/libs/PushNotificationsTest.kt @@ -19,7 +19,7 @@ class PushNotificationsTest : KSRobolectricTestCase() { fun messageThreadIntent() { val envelope = PushNotificationEnvelopeFactory.envelope() val messageThread = MessageThreadEnvelopeFactory.messageThreadEnvelope().messageThread() - val pushNotifications = PushNotifications(context, requireNotNull(environment().apiClient())) + val pushNotifications = PushNotifications(context, requireNotNull(environment().apiClientV2())) messageThread?.let { pushNotifications.messageThreadIntent(envelope, messageThread)