Skip to content

Commit

Permalink
Merge pull request #14691 from wordpress-mobile/feature/14639-push-no…
Browse files Browse the repository at this point in the history
…tification-workmanager

Setup for local push notifications
  • Loading branch information
planarvoid authored May 26, 2021
2 parents 455987b + 1a6116a commit 148df85
Show file tree
Hide file tree
Showing 12 changed files with 323 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
import org.wordpress.android.modules.DaggerAppComponentDebug;
import org.wordpress.android.util.AppLog;
import org.wordpress.android.util.AppLog.T;
import org.wordpress.android.util.UploadWorker;

public class WordPressDebug extends WordPress {
@Override
Expand All @@ -29,7 +28,7 @@ public void onCreate() {
protected void initWorkManager() {
Configuration config = (new Configuration.Builder())
.setMinimumLoggingLevel(Log.DEBUG)
.setWorkerFactory(new UploadWorker.Factory(mUploadStarter, mSiteStore))
.setWorkerFactory(mWordPressWorkerFactory)
.build();
WorkManager.initialize(this, config);
}
Expand Down
6 changes: 3 additions & 3 deletions WordPress/src/main/java/org/wordpress/android/WordPress.java
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,6 @@
import org.wordpress.android.util.QuickStartUtils;
import org.wordpress.android.util.RateLimitedTask;
import org.wordpress.android.util.SiteUtils;
import org.wordpress.android.util.UploadWorker;
import org.wordpress.android.util.UploadWorkerKt;
import org.wordpress.android.util.VolleyUtils;
import org.wordpress.android.util.WPActivityUtils;
Expand All @@ -119,6 +118,7 @@
import org.wordpress.android.util.config.AppConfig;
import org.wordpress.android.util.image.ImageManager;
import org.wordpress.android.widgets.AppRatingDialog;
import org.wordpress.android.workers.WordPressWorkersFactory;

import java.io.File;
import java.io.IOException;
Expand Down Expand Up @@ -183,6 +183,7 @@ public class WordPress extends MultiDexApplication implements HasAndroidInjector
@Inject AppConfig mAppConfig;
@Inject ImageEditorFileUtils mImageEditorFileUtils;
@Inject ExPlat mExPlat;
@Inject WordPressWorkersFactory mWordPressWorkerFactory;
@Inject @Named(APPLICATION_SCOPE) CoroutineScope mAppScope;

// For development and production `AnalyticsTrackerNosara`, for testing a mocked `Tracker` will be injected.
Expand Down Expand Up @@ -370,9 +371,8 @@ public void onConnectionSuspended(int i) {
}

protected void initWorkManager() {
UploadWorker.Factory factory = new UploadWorker.Factory(mUploadStarter, mSiteStore);
androidx.work.Configuration config =
(new androidx.work.Configuration.Builder()).setWorkerFactory(factory).build();
(new androidx.work.Configuration.Builder()).setWorkerFactory(mWordPressWorkerFactory).build();
WorkManager.initialize(this, config);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ object NotificationPushIds {
const val ACTIONS_PROGRESS_NOTIFICATION_ID = 50000
const val PENDING_DRAFTS_NOTIFICATION_ID = 600001
const val QUICK_START_REMINDER_NOTIFICATION_ID = 4001
const val LOCAL_NOTIFICATION_ID = 70000
const val ZENDESK_PUSH_NOTIFICATION_ID = 1999999999
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,11 @@ class UploadWorker(
workerClassName: String,
workerParameters: WorkerParameters
): ListenableWorker? {
// TODO This should use the [workerClassName] if there are other of Worker subclasses in the project
return UploadWorker(appContext, workerParameters, uploadStarter, siteStore)
return if (workerClassName == UploadWorker::class.java.name) {
UploadWorker(appContext, workerParameters, uploadStarter, siteStore)
} else {
null
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.wordpress.android.workers

import android.content.Context
import android.content.Intent
import org.wordpress.android.fluxc.store.AccountStore
import org.wordpress.android.fluxc.store.SiteStore
import javax.inject.Inject

class CreateSiteNotificationHandler @Inject constructor(
private val accountStore: AccountStore,
private val siteStore: SiteStore
) : LocalNotificationHandler {
override fun shouldShowNotification(): Boolean {
return accountStore.hasAccessToken() && !siteStore.hasSite()
}

override fun buildIntent(context: Context): Intent {
return Intent() // TODO: Replace this with respective intent in ActivityLauncher
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package org.wordpress.android.workers

import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import org.wordpress.android.push.NotificationPushIds
import java.util.concurrent.TimeUnit

data class LocalNotification(
val type: Type,
val delay: Long,
val delayUnits: TimeUnit,
@StringRes val title: Int,
@StringRes val text: Int,
@DrawableRes val icon: Int,
@DrawableRes val actionIcon: Int,
@StringRes val actionTitle: Int,
val uniqueId: Int? = null
) {
val id = uniqueId ?: NotificationPushIds.LOCAL_NOTIFICATION_ID + type.ordinal

enum class Type(val tag: String) {
CREATE_SITE("create_site");

companion object {
fun fromTag(tag: String?): Type? {
return when (tag) {
CREATE_SITE.tag -> CREATE_SITE
else -> null
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.wordpress.android.workers

import android.content.Context
import android.content.Intent
import org.wordpress.android.workers.LocalNotification.Type
import org.wordpress.android.workers.LocalNotification.Type.CREATE_SITE
import javax.inject.Inject

class LocalNotificationHandlerFactory @Inject constructor(
private val createSiteNotificationHandler: CreateSiteNotificationHandler
) {
fun buildLocalNotificationHandler(type: Type): LocalNotificationHandler {
return when (type) {
CREATE_SITE -> createSiteNotificationHandler
}
}
}

interface LocalNotificationHandler {
fun shouldShowNotification(): Boolean
fun buildIntent(context: Context): Intent
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package org.wordpress.android.workers

import androidx.core.app.NotificationManagerCompat
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import org.wordpress.android.viewmodel.ContextProvider
import org.wordpress.android.workers.LocalNotification.Type
import javax.inject.Inject

class LocalNotificationScheduler @Inject constructor(
private val contextProvider: ContextProvider,
private val localNotificationHandlerFactory: LocalNotificationHandlerFactory
) {
fun scheduleOneTimeNotification(localNotification: LocalNotification): Boolean {
val localPushHandler = localNotificationHandlerFactory.buildLocalNotificationHandler(localNotification.type)
if (localPushHandler.shouldShowNotification()) {
val work = OneTimeWorkRequestBuilder<LocalNotificationWorker>()
.setInitialDelay(localNotification.delay, localNotification.delayUnits)
.addTag(localNotification.type.tag)
.setInputData(
LocalNotificationWorker.buildData(
localNotification
)
)
.build()

WorkManager.getInstance(contextProvider.getContext()).enqueue(
work
)
return true
} else {
return false
}
}

fun cancelScheduledNotification(notificationType: Type) {
WorkManager.getInstance(contextProvider.getContext()).cancelAllWorkByTag(notificationType.tag)
}

fun cancelNotification(pushId: Int) {
if (pushId != -1) {
with(NotificationManagerCompat.from(contextProvider.getContext())) {
cancel(pushId)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package org.wordpress.android.workers

import android.app.PendingIntent
import android.content.Context
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.work.CoroutineWorker
import androidx.work.Data
import androidx.work.ListenableWorker
import androidx.work.WorkerFactory
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import org.wordpress.android.R
import org.wordpress.android.workers.LocalNotification.Type

class LocalNotificationWorker(
val context: Context,
params: WorkerParameters,
private val localNotificationHandlerFactory: LocalNotificationHandlerFactory
) : CoroutineWorker(context, params) {
@Suppress("TooGenericExceptionCaught")
override suspend fun doWork(): Result {
val id = inputData.getInt(ID, -1)

if (id == -1) return Result.failure()

with(NotificationManagerCompat.from(context)) {
notify(id, localNotificationBuilder().build())
}

return Result.success()
}

private fun localNotificationBuilder(): NotificationCompat.Builder {
val title = inputData.getInt(TITLE, -1)
val text = inputData.getInt(TEXT, -1)
val icon = inputData.getInt(ICON, -1)
val actionIcon = inputData.getInt(ACTION_ICON, -1)
val actionTitle = inputData.getInt(ACTION_TITLE, -1)

return NotificationCompat.Builder(context, context.getString(R.string.notification_channel_normal_id)).apply {
val pendingIntent = getPendingIntent()
setContentIntent(pendingIntent)
setSmallIcon(icon)
setContentTitle(context.getString(title))
setContentText(context.getString(text))
addAction(actionIcon, context.getString(actionTitle), pendingIntent)
priority = NotificationCompat.PRIORITY_DEFAULT
setCategory(NotificationCompat.CATEGORY_REMINDER)
setAutoCancel(true)
setColorized(true)
color = ContextCompat.getColor(context, R.color.blue_50)
}
}

private fun getPendingIntent(): PendingIntent {
val type = Type.fromTag(inputData.getString(TYPE))
val handler = type?.let { localNotificationHandlerFactory.buildLocalNotificationHandler(it) }
return PendingIntent.getActivity(
context,
0,
handler?.buildIntent(context),
PendingIntent.FLAG_CANCEL_CURRENT
)
}

class Factory(
private val localNotificationHandlerFactory: LocalNotificationHandlerFactory
) : WorkerFactory() {
override fun createWorker(
appContext: Context,
workerClassName: String,
workerParameters: WorkerParameters
): ListenableWorker? {
return if (workerClassName == LocalNotificationWorker::class.java.name) {
LocalNotificationWorker(appContext, workerParameters, localNotificationHandlerFactory)
} else {
null
}
}
}

companion object {
private const val TYPE = "key_type"
private const val ID = "key_id"
private const val TITLE = "key_title"
private const val TEXT = "key_text"
private const val ICON = "key_icon"
private const val ACTION_ICON = "key_action_icon"
private const val ACTION_TITLE = "key_action_title"

fun buildData(localNotification: LocalNotification): Data {
return workDataOf(
TYPE to localNotification.type.tag,
ID to localNotification.id,
TITLE to localNotification.title,
TEXT to localNotification.text,
ICON to localNotification.icon,
ACTION_ICON to localNotification.actionIcon,
ACTION_TITLE to localNotification.actionTitle
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.wordpress.android.workers

import androidx.work.DelegatingWorkerFactory
import org.wordpress.android.fluxc.store.SiteStore
import org.wordpress.android.ui.uploads.UploadStarter
import org.wordpress.android.util.UploadWorker
import javax.inject.Inject

class WordPressWorkersFactory @Inject constructor(
uploadStarter: UploadStarter,
siteStore: SiteStore,
localNotificationHandlerFactory: LocalNotificationHandlerFactory
) : DelegatingWorkerFactory() {
init {
addFactory(UploadWorker.Factory(uploadStarter, siteStore))
addFactory(LocalNotificationWorker.Factory(localNotificationHandlerFactory))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package org.wordpress.android.workers

import com.nhaarman.mockitokotlin2.whenever
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.junit.MockitoJUnitRunner
import org.wordpress.android.fluxc.store.AccountStore
import org.wordpress.android.fluxc.store.SiteStore

@RunWith(MockitoJUnitRunner::class)
class CreateSiteNotificationHandlerTest {
@Mock
lateinit var accountStore: AccountStore
@Mock
lateinit var siteStore: SiteStore

@Before
fun setUp() {
whenever(accountStore.hasAccessToken()).thenReturn(true)
whenever(siteStore.hasSite()).thenReturn(false)
}

@Test
fun verifyShouldShowNotification() {
assertThat(accountStore.hasAccessToken() && !siteStore.hasSite()).isTrue
}

@Test
fun verifyShouldNotShowNotification() {
assertThat(accountStore.hasAccessToken() && siteStore.hasSite()).isFalse
}

@Test
fun verifyDoesNotShowNotification() {
assertThat(!accountStore.hasAccessToken() && !siteStore.hasSite()).isFalse
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.wordpress.android.workers

import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.junit.MockitoJUnitRunner
import org.wordpress.android.workers.LocalNotification.Type.CREATE_SITE

@RunWith(MockitoJUnitRunner::class)
class LocalNotificationHandlerFactoryTest {
@Mock
lateinit var createSiteNotificationHandler: CreateSiteNotificationHandler

lateinit var localNotificationHandlerFactory: LocalNotificationHandlerFactory

@Before
fun setUp() {
localNotificationHandlerFactory = LocalNotificationHandlerFactory(createSiteNotificationHandler)
}

@Test
fun verifyLocalNotificationHandlerBuildsCorrectHandler() {
val handler = localNotificationHandlerFactory.buildLocalNotificationHandler(CREATE_SITE)
assertThat(handler).isInstanceOf(CreateSiteNotificationHandler::class.java)
}
}

0 comments on commit 148df85

Please sign in to comment.