From 2913fda461f9ffe198153eebf84ea6df18e6f22d Mon Sep 17 00:00:00 2001 From: Kuma Date: Thu, 1 Aug 2019 14:56:59 +0800 Subject: [PATCH 1/9] Fix Duplicate files copied in APK META-INF/library_release.kotlin_module. --- library/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/library/build.gradle b/library/build.gradle index f746bb3..2e2b710 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -56,6 +56,7 @@ android { compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 + kotlinOptions.freeCompilerArgs += ['-module-name', "notify"] } lintOptions.abortOnError false From 802ff951e0b6df0581a073afeaac1588a31e4070 Mon Sep 17 00:00:00 2001 From: Karn Saheb Date: Wed, 7 Aug 2019 01:56:10 -0400 Subject: [PATCH 2/9] Update module-name compiler arg to use full package name. --- library/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/build.gradle b/library/build.gradle index 2e2b710..2ade1ee 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -56,7 +56,7 @@ android { compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 - kotlinOptions.freeCompilerArgs += ['-module-name', "notify"] + kotlinOptions.freeCompilerArgs += ['-module-name', "io.karn.notify"] } lintOptions.abortOnError false From 70144f5d0f82e29464c5a4713084035176290826 Mon Sep 17 00:00:00 2001 From: Karn Saheb Date: Wed, 25 Sep 2019 20:31:01 -0400 Subject: [PATCH 3/9] Provide a new function with context and id args to cancel notifications. (#49) * Provide a new function to cancel notifications. This change also deprecates the original cancelNotification(id) function. * Assign default NotifyConfig.NotificationManager if none exists. --- library/src/main/java/io/karn/notify/Notify.kt | 14 ++++++++++++++ .../src/main/java/io/karn/notify/NotifyCreator.kt | 8 +++++++- library/src/test/java/io/karn/notify/NotifyTest.kt | 2 +- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/library/src/main/java/io/karn/notify/Notify.kt b/library/src/main/java/io/karn/notify/Notify.kt index 8c76d8e..d5c9351 100644 --- a/library/src/main/java/io/karn/notify/Notify.kt +++ b/library/src/main/java/io/karn/notify/Notify.kt @@ -108,9 +108,23 @@ class Notify internal constructor(internal var context: Context) { /** * Cancel an existing notification with a particular id. */ + @Deprecated(message = "NotificationManager might not have been initialized and can throw a NullPointerException -- provide a context.", + replaceWith = ReplaceWith("Notify.cancelNotification(context, id)")) + @Throws(NullPointerException::class) fun cancelNotification(id: Int) { return NotificationInterop.cancelNotification(Notify.defaultConfig.notificationManager!!, id) } + + /** + * Cancel an existing notification with a particular id. + */ + fun cancelNotification(context: Context, id: Int) { + if (defaultConfig.notificationManager == null) { + defaultConfig.notificationManager = context.applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + } + + return NotificationInterop.cancelNotification(defaultConfig.notificationManager!!, id) + } } init { diff --git a/library/src/main/java/io/karn/notify/NotifyCreator.kt b/library/src/main/java/io/karn/notify/NotifyCreator.kt index 4e16a3d..3eb42f5 100644 --- a/library/src/main/java/io/karn/notify/NotifyCreator.kt +++ b/library/src/main/java/io/karn/notify/NotifyCreator.kt @@ -181,8 +181,14 @@ class NotifyCreator internal constructor(private val notify: Notify) { * which provides the correct encapsulation of the this `cancel` function. */ @Deprecated(message = "Exposes function under the incorrect API -- NotifyCreator is reserved strictly for notification construction.", - replaceWith = ReplaceWith("Notify.cancelNotification(id)", "io.karn.notify.Notify")) + replaceWith = ReplaceWith( + "Notify.cancelNotification(context, id)", + "android.content.Context", "io.karn.notify.Notify")) + @Throws(NullPointerException::class) fun cancel(id: Int) { + // This should be safe to call from here because the Notify.with(context) function call + // would have initialized the NotificationManager object. In any case, the function has been + // annotated as one which can throw a NullPointerException. return Notify.cancelNotification(id) } } diff --git a/library/src/test/java/io/karn/notify/NotifyTest.kt b/library/src/test/java/io/karn/notify/NotifyTest.kt index 5b5720d..45d4505 100644 --- a/library/src/test/java/io/karn/notify/NotifyTest.kt +++ b/library/src/test/java/io/karn/notify/NotifyTest.kt @@ -125,7 +125,7 @@ class NotifyTest : NotifyTestBase() { Assert.assertEquals(1, NotificationInterop.getActiveNotifications(shadowNotificationManager).size) - Notify.cancelNotification(notificationId) + Notify.cancelNotification(context, notificationId) Assert.assertEquals(0, NotificationInterop.getActiveNotifications(shadowNotificationManager).size) } From 5847f76397b4bb4997b849005325137bd38a3ec3 Mon Sep 17 00:00:00 2001 From: Karn Saheb Date: Sun, 29 Sep 2019 01:16:48 -0400 Subject: [PATCH 4/9] Support for new Android Q Notification Bubbles (#50) * Initial bubbles support. * Add documentation for Bubbles. * Additional documentation and cleanup. * Fix robolectric for now. * Added two basic bubble notification tests. * Explicitly specify Jave 1.8 for Kotlin. --- gradle/configuration.gradle | 10 +- library/build.gradle | 6 +- .../main/java/io/karn/notify/NotifyCreator.kt | 77 +++++++++---- .../java/io/karn/notify/entities/Payload.kt | 44 ++++++++ .../notify/internal/NotificationInterop.kt | 13 +++ .../karn/notify/internal/RawNotification.kt | 1 + .../karn/notify/internal/utils/Annotations.kt | 5 + .../io/karn/notify/internal/utils/Errors.kt | 2 + .../java/io/karn/notify/NotifyBubblizeTest.kt | 106 ++++++++++++++++++ .../java/io/karn/notify/NotifyContentTest.kt | 11 +- .../io/karn/notify/NotifyStackableTest.kt | 6 +- .../src/test/resources/robolectric.properties | 2 + sample/src/main/AndroidManifest.xml | 12 +- .../main/java/presentation/BubbleActivity.kt | 37 ++++++ .../main/java/presentation/MainActivity.kt | 31 ++++- .../src/main/res/layout/activity_bubble.xml | 46 ++++++++ sample/src/main/res/layout/activity_main.xml | 7 ++ 17 files changed, 376 insertions(+), 40 deletions(-) create mode 100644 library/src/test/java/io/karn/notify/NotifyBubblizeTest.kt create mode 100644 library/src/test/resources/robolectric.properties create mode 100644 sample/src/main/java/presentation/BubbleActivity.kt create mode 100644 sample/src/main/res/layout/activity_bubble.xml diff --git a/gradle/configuration.gradle b/gradle/configuration.gradle index 83f549d..a828c83 100644 --- a/gradle/configuration.gradle +++ b/gradle/configuration.gradle @@ -11,7 +11,7 @@ def versions = [ libName: '1.3.0', kotlin: '1.3.11', - core: '1.0.1', + core: '1.2.0-alpha04', appcompat: '1.0.2', jacoco: '0.8.2', @@ -19,8 +19,8 @@ def versions = [ ] def build = [ - compileSdk: 28, - targetSdk: 28, + compileSdk: 29, + targetSdk: 29, minSdk: 19, jacocoAgentVersion: versions.jacoco, @@ -40,7 +40,7 @@ def dependencies = [ reflect: "org.jetbrains.kotlin:kotlin-reflect:${versions.kotlin}" ], androidx: [ - core: "androidx.core:core:${versions.core}", + core: "androidx.core:core-ktx:${versions.core}", appcompat: "androidx.appcompat:appcompat:${versions.appcompat}" ] ] @@ -48,7 +48,7 @@ def dependencies = [ def testDependencies = [ instrumentationRunner: 'androidx.test.runner.AndroidJUnitRunner', junit: 'junit:junit:4.12', - robolectric: 'org.robolectric:robolectric:4.0' + robolectric: 'org.robolectric:robolectric:4.3' ] ext.config = [ diff --git a/library/build.gradle b/library/build.gradle index 2ade1ee..6ef5d16 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -56,7 +56,11 @@ android { compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 - kotlinOptions.freeCompilerArgs += ['-module-name', "io.karn.notify"] + } + // For Kotlin projects + kotlinOptions { + freeCompilerArgs += ['-module-name', "io.karn.notify"] + jvmTarget = "1.8" } lintOptions.abortOnError false diff --git a/library/src/main/java/io/karn/notify/NotifyCreator.kt b/library/src/main/java/io/karn/notify/NotifyCreator.kt index 3eb42f5..8dd3e5c 100644 --- a/library/src/main/java/io/karn/notify/NotifyCreator.kt +++ b/library/src/main/java/io/karn/notify/NotifyCreator.kt @@ -24,11 +24,14 @@ package io.karn.notify +import android.annotation.TargetApi +import android.os.Build import androidx.core.app.NotificationCompat import io.karn.notify.entities.Payload import io.karn.notify.internal.RawNotification import io.karn.notify.internal.utils.Action import io.karn.notify.internal.utils.Errors +import io.karn.notify.internal.utils.Experimental import io.karn.notify.internal.utils.NotifyScopeMarker /** @@ -42,6 +45,7 @@ class NotifyCreator internal constructor(private val notify: Notify) { private var header = Notify.defaultConfig.defaultHeader.copy() private var content: Payload.Content = Payload.Content.Default() private var actions: ArrayList? = null + private var bubblize: Payload.Bubble? = null private var stackable: Payload.Stackable? = null /** @@ -63,8 +67,7 @@ class NotifyCreator internal constructor(private val notify: Notify) { */ fun alerting(key: String, init: Payload.Alerts.() -> Unit): NotifyCreator { // Clone object and assign the key. - this.alerts = this.alerts.copy(channelKey = key) - this.alerts.init() + this.alerts = this.alerts.copy(channelKey = key).also(init) return this } @@ -82,8 +85,7 @@ class NotifyCreator internal constructor(private val notify: Notify) { * Scoped function for modifying the content of a 'Default' notification. */ fun content(init: Payload.Content.Default.() -> Unit): NotifyCreator { - this.content = Payload.Content.Default() - (this.content as Payload.Content.Default).init() + this.content = Payload.Content.Default().also(init) return this } @@ -91,8 +93,7 @@ class NotifyCreator internal constructor(private val notify: Notify) { * Scoped function for modifying the content of a 'TextList' notification. */ fun asTextList(init: Payload.Content.TextList.() -> Unit): NotifyCreator { - this.content = Payload.Content.TextList() - (this.content as Payload.Content.TextList).init() + this.content = Payload.Content.TextList().also(init) return this } @@ -100,8 +101,7 @@ class NotifyCreator internal constructor(private val notify: Notify) { * Scoped function for modifying the content of a 'BigText' notification. */ fun asBigText(init: Payload.Content.BigText.() -> Unit): NotifyCreator { - this.content = Payload.Content.BigText() - (this.content as Payload.Content.BigText).init() + this.content = Payload.Content.BigText().also(init) return this } @@ -109,8 +109,7 @@ class NotifyCreator internal constructor(private val notify: Notify) { * Scoped function for modifying the content of a 'BigPicture' notification. */ fun asBigPicture(init: Payload.Content.BigPicture.() -> Unit): NotifyCreator { - this.content = Payload.Content.BigPicture() - (this.content as Payload.Content.BigPicture).init() + this.content = Payload.Content.BigPicture().also(init) return this } @@ -118,8 +117,7 @@ class NotifyCreator internal constructor(private val notify: Notify) { * Scoped function for modifying the content of a 'Message' notification. */ fun asMessage(init: Payload.Content.Message.() -> Unit): NotifyCreator { - this.content = Payload.Content.Message() - (this.content as Payload.Content.Message).init() + this.content = Payload.Content.Message().also(init) return this } @@ -128,8 +126,46 @@ class NotifyCreator internal constructor(private val notify: Notify) { * relies on adding standard notification Action objects. */ fun actions(init: ArrayList.() -> Unit): NotifyCreator { - this.actions = ArrayList() - (this.actions as ArrayList).init() + this.actions = ArrayList().also(init) + return this + } + + /** + * Scoped function for modifying the behaviour of 'Bubble' notifications. The transformation + * relies on the 'bubbleIcon' and 'targetActivity' values which are used to create the Bubble. + * + * Note that Bubbles have very specific restrictions in terms of when they can be shown to the + * user. In particular, at least one of the following conditions must be met before the + * notification is shown. + * - The notification uses MessagingStyle, and has a Person added. + * - The notification is from a call to Service.startForeground, has a category of + * CATEGORY_CALL, and has a Person added. + * - The app is in the foreground when the notification is sent. + * + * In addition, the 'Bubbles' flag has to be enabled from the Android Developer Options in the + * Settings of the Device for the notifications to be shown as Bubbles. + * + * Finally, the 'targetActivity' should also have the following attributes to correctly show a + * Bubble notification. + * + * android:documentLaunchMode="always" + * android:resizeableActivity="true" + * android:screenOrientation="portrait" + * + */ + @Experimental + @TargetApi(Build.VERSION_CODES.Q) + fun bubblize(init: Payload.Bubble.() -> Unit): NotifyCreator { + this.bubblize = Payload.Bubble().also(init) + + this.bubblize!! + .takeUnless { it.bubbleIcon == null } + ?: throw IllegalArgumentException(Errors.INVALID_BUBBLE_ICON_ERROR) + + this.bubblize!! + .takeUnless { it.targetActivity == null } + ?: throw IllegalArgumentException(Errors.INVALID_BUBBLE_TARGET_ACTIVITY_ERROR) + return this } @@ -138,14 +174,11 @@ class NotifyCreator internal constructor(private val notify: Notify) { * relies on the 'summaryText' of a stackable notification. */ fun stackable(init: Payload.Stackable.() -> Unit): NotifyCreator { - this.stackable = Payload.Stackable() - (this.stackable as Payload.Stackable).init() + this.stackable = Payload.Stackable().also(init) - this.stackable - ?.takeIf { it.key.isNullOrEmpty() } - ?.apply { - throw IllegalArgumentException(Errors.INVALID_STACK_KEY_ERROR) - } + this.stackable!! + .takeUnless { it.key.isNullOrEmpty() } + ?: throw IllegalArgumentException(Errors.INVALID_STACK_KEY_ERROR) return this } @@ -155,7 +188,7 @@ class NotifyCreator internal constructor(private val notify: Notify) { * transformations (if any) from the {@see NotifyCreator} builder object. */ fun asBuilder(): NotificationCompat.Builder { - return notify.asBuilder(RawNotification(meta, alerts, header, content, stackable, actions)) + return notify.asBuilder(RawNotification(meta, alerts, header, content, bubblize, stackable, actions)) } /** diff --git a/library/src/main/java/io/karn/notify/entities/Payload.kt b/library/src/main/java/io/karn/notify/entities/Payload.kt index bace5ae..2199974 100644 --- a/library/src/main/java/io/karn/notify/entities/Payload.kt +++ b/library/src/main/java/io/karn/notify/entities/Payload.kt @@ -31,6 +31,7 @@ import android.net.Uri import androidx.annotation.ColorInt import androidx.annotation.DrawableRes import androidx.core.app.NotificationCompat +import androidx.core.graphics.drawable.IconCompat import io.karn.notify.Notify import io.karn.notify.R import io.karn.notify.internal.utils.Action @@ -272,6 +273,49 @@ sealed class Payload { ) : Content(), SupportsLargeIcon } + /** + * Contains configuration for Android Q Bubbles which are a native implementation of the + * chatheads functionality pioneered by Facebook. The documentation around Bubbles describes + * them as follows: + * "Bubbles let users easily multi-task from anywhere on their device. They are designed to be + * an alternative to using SYSTEM_ALERT_WINDOW." + * + * Bubbles | Android Developers + * + * Note that you can only have a total of five Bubbles being shown at any time. + */ + data class Bubble( + /** + * A pending intent which contains a reference to the Activity that is being created + * once the bubble has been created. + */ + var targetActivity: PendingIntent? = null, + /** + * A pending intent which is to be fired when the Bubble is dismissed/closed. + */ + var clearIntent: PendingIntent? = null, + /** + * A configuration which defines the height of the container which holds the Activity + * that is being show. + */ + var desiredHeight: Int = 600, + /** + * The icon which will be used by the bubble. + */ + var bubbleIcon: IconCompat? = null, + /** + * Flag to auto-expand the Bubble to create and display the Activity defined by the + * PendingIntent. This flag has no effect when the app is in the background. + */ + var autoExpand: Boolean = false, + /** + * Flag to hide the initial notification in the notification shade which the + * notification is shown from the foreground. This flag has no effect when the app is in + * the background and the initial notification is shown regardless. + */ + var suppressInitialNotification: Boolean = false + ) + /** * Contains configuration specific to the manual stacking behaviour of a notification. * Manual stacking occurs for all notifications with the same key, additionally the summary diff --git a/library/src/main/java/io/karn/notify/internal/NotificationInterop.kt b/library/src/main/java/io/karn/notify/internal/NotificationInterop.kt index 9d52c15..68fa76b 100644 --- a/library/src/main/java/io/karn/notify/internal/NotificationInterop.kt +++ b/library/src/main/java/io/karn/notify/internal/NotificationInterop.kt @@ -207,6 +207,19 @@ internal object NotificationInterop { } } + payload.bubblize + ?.takeIf { Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q } + ?.also { + builder.bubbleMetadata = NotificationCompat.BubbleMetadata.Builder() + .setDesiredHeight(it.desiredHeight) + .setIntent(it.targetActivity!!) + .setIcon(it.bubbleIcon!!) + .setAutoExpandBubble(it.autoExpand) + .setSuppressNotification(it.suppressInitialNotification) + .setDeleteIntent(it.clearIntent) + .build() + } + var style: NotificationCompat.Style? = null payload.stackable?.let { diff --git a/library/src/main/java/io/karn/notify/internal/RawNotification.kt b/library/src/main/java/io/karn/notify/internal/RawNotification.kt index 7fc6144..f0f3b21 100644 --- a/library/src/main/java/io/karn/notify/internal/RawNotification.kt +++ b/library/src/main/java/io/karn/notify/internal/RawNotification.kt @@ -32,6 +32,7 @@ internal data class RawNotification( internal val alerting: Payload.Alerts, internal val header: Payload.Header, internal val content: Payload.Content, + internal val bubblize: Payload.Bubble?, internal val stackable: Payload.Stackable?, internal val actions: ArrayList? ) diff --git a/library/src/main/java/io/karn/notify/internal/utils/Annotations.kt b/library/src/main/java/io/karn/notify/internal/utils/Annotations.kt index 411a409..619ed14 100644 --- a/library/src/main/java/io/karn/notify/internal/utils/Annotations.kt +++ b/library/src/main/java/io/karn/notify/internal/utils/Annotations.kt @@ -27,6 +27,11 @@ package io.karn.notify.internal.utils import androidx.annotation.IntDef import io.karn.notify.Notify +/** + * Denotes features which are considered experimental and are subject to change without notice. + */ +annotation class Experimental + @DslMarker annotation class NotifyScopeMarker diff --git a/library/src/main/java/io/karn/notify/internal/utils/Errors.kt b/library/src/main/java/io/karn/notify/internal/utils/Errors.kt index f42dea2..b3ba05a 100644 --- a/library/src/main/java/io/karn/notify/internal/utils/Errors.kt +++ b/library/src/main/java/io/karn/notify/internal/utils/Errors.kt @@ -26,4 +26,6 @@ package io.karn.notify.internal.utils internal object Errors { const val INVALID_STACK_KEY_ERROR = "Invalid stack key provided." + const val INVALID_BUBBLE_ICON_ERROR = "Invalid bubble icon provided." + const val INVALID_BUBBLE_TARGET_ACTIVITY_ERROR = "Invalid target activity provided." } diff --git a/library/src/test/java/io/karn/notify/NotifyBubblizeTest.kt b/library/src/test/java/io/karn/notify/NotifyBubblizeTest.kt new file mode 100644 index 0000000..2ee830c --- /dev/null +++ b/library/src/test/java/io/karn/notify/NotifyBubblizeTest.kt @@ -0,0 +1,106 @@ +/* + * MIT License + * + * Copyright (c) 2018 Karn Saheb + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.karn.notify + +import android.app.PendingIntent +import android.content.Intent +import android.provider.Settings +import androidx.core.app.NotificationCompat +import androidx.core.graphics.drawable.IconCompat +import io.karn.notify.internal.NotificationInterop +import io.karn.notify.internal.NotifyExtender +import io.karn.notify.internal.utils.Action +import io.karn.notify.internal.utils.Errors +import org.junit.Assert +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class NotifyBubblizeTest : NotifyTestBase() { + + @Test + fun defaultBubblizeTest() { + val testTitle = "New dessert menu" + val testText = "The Cheesecake Factory has a new dessert for you to try!" + + val notification = Notify.with(this.context) + .content { + title = testTitle + text = testText + } + .asBuilder() + .build() + + Assert.assertEquals(testTitle, notification.extras.getCharSequence(NotificationCompat.EXTRA_TITLE)) + Assert.assertEquals(testText, notification.extras.getCharSequence(NotificationCompat.EXTRA_TEXT)) + } + + @Test + fun invalidRequiredArgsTest() { + val testTitle = "New dessert menu" + val testText = "The Cheesecake Factory has a new dessert for you to try!" + + var exceptionThrown: IllegalArgumentException? = null + + try { + Notify.with(this.context) + .content { + title = testTitle + text = testText + } + .bubblize { + // bubbleIcon + targetActivity = PendingIntent.getActivity(this@NotifyBubblizeTest.context, 0, Intent(Settings.ACTION_SETTINGS), 0) + } + .asBuilder() + .build() + } catch (e: IllegalArgumentException) { + exceptionThrown = e + } + + Assert.assertNotNull(exceptionThrown) + Assert.assertEquals(Errors.INVALID_BUBBLE_ICON_ERROR, exceptionThrown?.message) + + try { + Notify.with(this.context) + .content { + title = testTitle + text = testText + } + .bubblize { + bubbleIcon = IconCompat.createWithResource(this@NotifyBubblizeTest.context, R.drawable.ic_app_icon) + // targetActivity + } + .asBuilder() + .build() + } catch (e: IllegalArgumentException) { + exceptionThrown = e + } + + Assert.assertNotNull(exceptionThrown) + Assert.assertEquals(Errors.INVALID_BUBBLE_TARGET_ACTIVITY_ERROR, exceptionThrown?.message) + } +} diff --git a/library/src/test/java/io/karn/notify/NotifyContentTest.kt b/library/src/test/java/io/karn/notify/NotifyContentTest.kt index f2f7198..22ec4e1 100644 --- a/library/src/test/java/io/karn/notify/NotifyContentTest.kt +++ b/library/src/test/java/io/karn/notify/NotifyContentTest.kt @@ -129,7 +129,7 @@ class NotifyContentTest { Assert.assertEquals(testTitle, notification.extras.getCharSequence(NotificationCompat.EXTRA_TITLE).toString()) Assert.assertEquals(testText, notification.extras.getCharSequence(NotificationCompat.EXTRA_TEXT).toString()) - Assert.assertEquals(testLines, notification.extras.getCharSequenceArray(NotificationCompat.EXTRA_TEXT_LINES).toList()) + Assert.assertEquals(testLines, notification.extras.getCharSequenceArray(NotificationCompat.EXTRA_TEXT_LINES)?.toList()) } @Test @@ -185,10 +185,10 @@ class NotifyContentTest { Assert.assertEquals(testCollapsedText, notification.extras.getCharSequence(NotificationCompat.EXTRA_SUMMARY_TEXT)) // Assert.assertEquals(context.resources.getDrawable(testLargeIconResID, context.theme), notification.getLargeIcon().loadDrawable(this.context)) - val actualIcon: Icon = notification.extras.getParcelable(NotificationCompat.EXTRA_LARGE_ICON) + val actualIcon: Icon? = notification.extras.getParcelable(NotificationCompat.EXTRA_LARGE_ICON) Assert.assertNotNull(actualIcon) - val actualImage: Bitmap = notification.extras.getParcelable(NotificationCompat.EXTRA_PICTURE) + val actualImage: Bitmap? = notification.extras.getParcelable(NotificationCompat.EXTRA_PICTURE) Assert.assertNotNull(actualImage) Assert.assertEquals(testImage, actualImage) @@ -227,8 +227,9 @@ class NotifyContentTest { Assert.assertEquals(testUserDisplayName, notification.extras.getCharSequence(NotificationCompat.EXTRA_SELF_DISPLAY_NAME)) Assert.assertEquals(testConversationTitle, notification.extras.getCharSequence(NotificationCompat.EXTRA_CONVERSATION_TITLE)) - val actualMessages = getMessagesFromBundleArray(notification.extras.getParcelableArray(NotificationCompat.EXTRA_MESSAGES)) - Assert.assertEquals(testMessages.size, actualMessages.size) + val actualMessages = notification.extras.getParcelableArray(NotificationCompat.EXTRA_MESSAGES)?.let { getMessagesFromBundleArray(it) } + Assert.assertNotNull(actualMessages) + Assert.assertEquals(testMessages.size, actualMessages!!.size) actualMessages.forEach { message -> testMessages[actualMessages.indexOf(message)].let { diff --git a/library/src/test/java/io/karn/notify/NotifyStackableTest.kt b/library/src/test/java/io/karn/notify/NotifyStackableTest.kt index 0dfe9ca..7f00648 100644 --- a/library/src/test/java/io/karn/notify/NotifyStackableTest.kt +++ b/library/src/test/java/io/karn/notify/NotifyStackableTest.kt @@ -176,14 +176,14 @@ class NotifyStackableTest : NotifyTestBase() { .build() // Assert.assertEquals("android.app.Notification\$InboxStyle", notification.extras.getCharSequence(NotificationCompat.EXTRA_TEMPLATE).toString()) - Assert.assertEquals("3" + testSummaryTitle, notification.extras.getCharSequence(NotificationCompat.EXTRA_TITLE)) + Assert.assertEquals("3$testSummaryTitle", notification.extras.getCharSequence(NotificationCompat.EXTRA_TITLE)) Assert.assertEquals(testSummaryText, notification.extras.getCharSequence(NotificationCompat.EXTRA_TEXT).toString()) Assert.assertEquals(testKey, NotifyExtender.getKey(notification.extras)) Assert.assertEquals(testClickIntent, notification.contentIntent) Assert.assertEquals(testSummaryContent, NotifyExtender.getExtensions(notification.extras).getCharSequence(NotifyExtender.SUMMARY_CONTENT)) Assert.assertEquals( - Arrays.asList(testSummaryContent, testSummaryContent, testSummaryContent), - notification.extras.getCharSequenceArray(NotificationCompat.EXTRA_TEXT_LINES).toList()) + listOf(testSummaryContent, testSummaryContent, testSummaryContent), + notification.extras.getCharSequenceArray(NotificationCompat.EXTRA_TEXT_LINES)?.toList()) Assert.assertEquals(1, notification.actions.size) Assert.assertEquals(testActionText, notification.actions.first().title) Assert.assertEquals(testActionIntent, notification.actions.first().actionIntent) diff --git a/library/src/test/resources/robolectric.properties b/library/src/test/resources/robolectric.properties new file mode 100644 index 0000000..4bc073d --- /dev/null +++ b/library/src/test/resources/robolectric.properties @@ -0,0 +1,2 @@ +# Target SDK 28. Robolectric doesn't currently support Android Q. +sdk=28 diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index 2547d4c..b25b6e2 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -26,14 +26,11 @@ - - + + + + diff --git a/sample/src/main/java/presentation/BubbleActivity.kt b/sample/src/main/java/presentation/BubbleActivity.kt new file mode 100644 index 0000000..507cfdf --- /dev/null +++ b/sample/src/main/java/presentation/BubbleActivity.kt @@ -0,0 +1,37 @@ +/* + * MIT License + * + * Copyright (c) 2018 Karn Saheb + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package presentation + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import io.karn.notify.sample.R + +class BubbleActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_bubble) + } +} diff --git a/sample/src/main/java/presentation/MainActivity.kt b/sample/src/main/java/presentation/MainActivity.kt index 955eef5..83025e6 100644 --- a/sample/src/main/java/presentation/MainActivity.kt +++ b/sample/src/main/java/presentation/MainActivity.kt @@ -24,15 +24,20 @@ package presentation +import android.app.PendingIntent +import android.content.Intent import android.graphics.BitmapFactory import android.graphics.Color +import android.os.Build import android.os.Bundle import android.view.View +import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.core.app.NotificationCompat +import androidx.core.graphics.drawable.IconCompat import io.karn.notify.Notify import io.karn.notify.sample.R -import java.util.* +import java.util.Arrays class MainActivity : AppCompatActivity() { @@ -126,4 +131,28 @@ class MainActivity : AppCompatActivity() { } .show() } + + fun notifyBubble(view: View) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + Toast.makeText(this, "Notification Bubbles are only supported on a device running Android Q or later.", Toast.LENGTH_SHORT).show() + return + } + + Notify + .with(this) + .content { + title = "New dessert menu" + text = "The Cheesecake Factory has a new dessert for you to try!" + } + .bubblize { + // Create bubble intent + val target = Intent(this@MainActivity, BubbleActivity::class.java) + val bubbleIntent = PendingIntent.getActivity(this@MainActivity, 0, target, 0 /* flags */) + + bubbleIcon = IconCompat.createWithResource(this@MainActivity, R.drawable.ic_app_icon) + targetActivity = bubbleIntent + suppressInitialNotification = true + } + .show() + } } diff --git a/sample/src/main/res/layout/activity_bubble.xml b/sample/src/main/res/layout/activity_bubble.xml new file mode 100644 index 0000000..675201a --- /dev/null +++ b/sample/src/main/res/layout/activity_bubble.xml @@ -0,0 +1,46 @@ + + + + + + + + + diff --git a/sample/src/main/res/layout/activity_main.xml b/sample/src/main/res/layout/activity_main.xml index 9815a2c..3fc40c8 100644 --- a/sample/src/main/res/layout/activity_main.xml +++ b/sample/src/main/res/layout/activity_main.xml @@ -72,4 +72,11 @@ android:layout_height="wrap_content" android:onClick="notifyMessage" android:text="Notify messages!" /> + + From baa620194ec57f84652394d2b98b15ef702487ab Mon Sep 17 00:00:00 2001 From: Karn Saheb Date: Wed, 16 Oct 2019 18:59:04 -0400 Subject: [PATCH 5/9] Bubble notification docs (#51) * Updated Advanced Notification Guide to include Bubble notifications. * String formatting and cleanup * Switched to 100col width for comments. --- docs/advanced.md | 76 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 63 insertions(+), 13 deletions(-) diff --git a/docs/advanced.md b/docs/advanced.md index 79a2970..14a1909 100644 --- a/docs/advanced.md +++ b/docs/advanced.md @@ -58,9 +58,11 @@ Notify add(Action( // The icon corresponding to the action. R.drawable.ic_app_icon, - // The text corresponding to the action -- this is what shows . + // The text corresponding to the action -- this is what shows below the + // notification. "Clear", - // Swap this PendingIntent for whatever Intent is to be processed when the action is clicked. + // Swap this PendingIntent for whatever Intent is to be processed when the action + // is clicked. PendingIntent.getService(context, 0, Intent(context, MyNotificationService::class.java) @@ -84,21 +86,69 @@ Notify title = "New dessert menu" text = "The Cheesecake Factory has a new dessert for you to try!" } - // Define the notification as being stackable. This block should be the same for all notifications which - // are to be grouped together. + // Define the notification as being stackable. This block should be the same for all + // notifications which are to be grouped together. .stackable { // this: Payload.Stackable - // In particular, this key should be the same. The properties of this stackable notification as - // taken from the latest stackable notification's stackable block. + // In particular, this key should be the same. The properties of this stackable + // notification as taken from the latest stackable notification's stackable block. key = "test_key" - // This is the summary of this notification as it appears when it is as part of a stacked notification. This - // String value is what is shown as a single line in the stacked notification. + // This is the summary of this notification as it appears when it is as part of a + // stacked notification. This String value is what is shown as a single line in the + // stacked notification. summaryContent = "test summary content" - // The number of notifications with the same key is passed as the 'count' argument. We happen not to - // use it, but it is there if needed. + // The number of notifications with the same key is passed as the 'count' argument. We + // happen not to use it, but it is there if needed. summaryTitle = { count -> "Summary title" } - // ... here as well, but we instead choose to use to to update the summary for when the notification - // is collapsed. + // ... here as well, but we instead choose to use to to update the summary for when the + // notification is collapsed. summaryDescription = { count -> count.toString() + " new notifications." } } .show() -``` \ No newline at end of file +``` + + +#### BUBBLE NOTIFICATIONS + +With the release of Android 10, Notify now also supports [Notification Bubbles](https://developer.android.com/guide/topics/ui/bubbles) on devices with the `Notification Bubbles` enabled through the Developer Settings. This new form of notification allows an application to display rich content from an activity at a glance to a user. + +Begin by first creating an activity and adding the following permissions to that activity within your `AndroidManifest.xml`: + +```xml + + + + + android:allowEmbedded="true" + android:documentLaunchMode="always" + android:resizeableActivity="true" /> + +``` + +Then you can target that activity from the notification. + +```kotlin +Notify.with(context) + .content { // this: Payload.Content.Default + title = "New dessert menu" + text = "The Cheesecake Factory has a new dessert for you to try!" + } + // Define the Notification as supporting a Bubble format. This style can be applied to any + // notification. + .bubblize { // this: Payload.Bubble + // Configure the target Intent for the Notification to launch when it is expanded. + val target = Intent(context, BubbleActivity::class.java) + // Provide a PendingIntent to launch the above target once the Bubble is expanded. + val bubbleIntent = PendingIntent.getActivity(context, 0, target, 0) + + // Set the image for the Bubble, this uses the IconCompat class to build the icon being + // shown within the bubble. + bubbleIcon = IconCompat.createWithResource(context, R.drawable.ic_app_icon) + // Set the activity that is being shown when the Bubble is expanded to the PendingIntent + // created above. + targetActivity = bubbleIntent + } + .show() +``` From a0244608981f65954485ccbce70e823454d0a416 Mon Sep 17 00:00:00 2001 From: Jose Martinez <22124150+jojemapa@users.noreply.github.com> Date: Sat, 28 Mar 2020 15:23:14 -0700 Subject: [PATCH 6/9] Support for progress notifications (#58) * Support for progress notifications * update docs * Update types.md * Proposed changes and docs --- docs/assets/types/progress.png | Bin 0 -> 57152 bytes docs/types.md | 26 +++++++++++++- .../main/java/io/karn/notify/NotifyCreator.kt | 8 ++++- .../io/karn/notify/entities/NotifyConfig.kt | 9 +++++ .../java/io/karn/notify/entities/Payload.kt | 24 +++++++++++++ .../notify/internal/NotificationInterop.kt | 5 +++ .../karn/notify/internal/RawNotification.kt | 3 +- .../main/java/presentation/MainActivity.kt | 32 ++++++++++++++++++ sample/src/main/res/layout/activity_main.xml | 14 ++++++++ 9 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 docs/assets/types/progress.png diff --git a/docs/assets/types/progress.png b/docs/assets/types/progress.png new file mode 100644 index 0000000000000000000000000000000000000000..67ee1d76366f29637a6d3cfedd5915b2fe18591f GIT binary patch literal 57152 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 literal 0 HcmV?d00001 diff --git a/docs/types.md b/docs/types.md index 7d33df4..4ad37d4 100644 --- a/docs/types.md +++ b/docs/types.md @@ -117,4 +117,28 @@ Notify ) } .show() -``` \ No newline at end of file +``` + +#### PROGRESS NOTIFICATION + +![Progress notification](./assets/types/progress.png) + +Progress notification is useful when you need to display information about the detail of a process such as uploading a file to a server, or some calculation that takes time and you want to keep the user informed. You can ser `showProgress` true to display it, and if you need determinate progress you can set `enablePercentage` true and specify `progressPercent` to your current value + +```Kotlin +Notify + .with(context) + .asBigText { + title = "Uploading files" + expandedText = "The files are being uploaded!" + bigText = "Daft Punk - Get Lucky.flac is uploading to server /music/favorites" + } + .progress { + showProgress = true + + //For determinate progress + //enablePercentage = true + //progressPercent = 27 + } + .show() +``` diff --git a/library/src/main/java/io/karn/notify/NotifyCreator.kt b/library/src/main/java/io/karn/notify/NotifyCreator.kt index 8dd3e5c..1ea9d5c 100644 --- a/library/src/main/java/io/karn/notify/NotifyCreator.kt +++ b/library/src/main/java/io/karn/notify/NotifyCreator.kt @@ -47,6 +47,7 @@ class NotifyCreator internal constructor(private val notify: Notify) { private var actions: ArrayList? = null private var bubblize: Payload.Bubble? = null private var stackable: Payload.Stackable? = null + private var progress: Payload.Progress = Payload.Progress() /** * Scoped function for modifying the Metadata of a notification, such as click intents, @@ -81,6 +82,11 @@ class NotifyCreator internal constructor(private val notify: Notify) { return this } + fun progress(init: Payload.Progress.() -> Unit): NotifyCreator { + this.progress.init() + return this + } + /** * Scoped function for modifying the content of a 'Default' notification. */ @@ -188,7 +194,7 @@ class NotifyCreator internal constructor(private val notify: Notify) { * transformations (if any) from the {@see NotifyCreator} builder object. */ fun asBuilder(): NotificationCompat.Builder { - return notify.asBuilder(RawNotification(meta, alerts, header, content, bubblize, stackable, actions)) + return notify.asBuilder(RawNotification(meta, alerts, header, content, bubblize, stackable, actions, progress)) } /** diff --git a/library/src/main/java/io/karn/notify/entities/NotifyConfig.kt b/library/src/main/java/io/karn/notify/entities/NotifyConfig.kt index 41d1d9e..15c4092 100644 --- a/library/src/main/java/io/karn/notify/entities/NotifyConfig.kt +++ b/library/src/main/java/io/karn/notify/entities/NotifyConfig.kt @@ -39,6 +39,10 @@ data class NotifyConfig( * and notification color.) */ internal var defaultHeader: Payload.Header = Payload.Header(), + /** + * Specifies the default configuration of a progress (e.g the default progress type) + */ + internal var defaultProgress: Payload.Progress = Payload.Progress(), /** * Specifies the default alerting configuration for notifications. */ @@ -55,4 +59,9 @@ data class NotifyConfig( defaultAlerting.init() return this } + + fun progress(init: Payload.Progress.() -> Unit): NotifyConfig { + defaultProgress.init() + return this + } } diff --git a/library/src/main/java/io/karn/notify/entities/Payload.kt b/library/src/main/java/io/karn/notify/entities/Payload.kt index 2199974..6fc911e 100644 --- a/library/src/main/java/io/karn/notify/entities/Payload.kt +++ b/library/src/main/java/io/karn/notify/entities/Payload.kt @@ -167,6 +167,30 @@ sealed class Payload { var showTimestamp: Boolean = true ) + /** + * Contains configuration that is specific to the progress of a notification, inder + */ + class Progress constructor( + + /** + * The default false for a indeterminate horizontal progress in notification. + * If this is true the notification show horizontal progress with exact value + */ + var enablePercentage: Boolean = false, + + /* + * The value of progress percent + * */ + var progressPercent: Int = 0, + + /** + * The default false for simple notiffication + * If this is true the notification show progress + */ + var showProgress: Boolean = false + + ) + /** * Deterministic property assignment for a notification type. */ diff --git a/library/src/main/java/io/karn/notify/internal/NotificationInterop.kt b/library/src/main/java/io/karn/notify/internal/NotificationInterop.kt index 68fa76b..8f17cb9 100644 --- a/library/src/main/java/io/karn/notify/internal/NotificationInterop.kt +++ b/library/src/main/java/io/karn/notify/internal/NotificationInterop.kt @@ -146,6 +146,11 @@ internal object NotificationInterop { // The duration of time after which the notification is automatically dismissed. .setTimeoutAfter(payload.meta.timeout) + if (payload.progress.showProgress) { + if (payload.progress.enablePercentage) builder.setProgress(100,payload.progress.progressPercent,false) + else builder.setProgress(0,0,true) + } + // Add contacts if any -- will help display prominently if possible. payload.meta.contacts.takeIf { it.isNotEmpty() }?.forEach { builder.addPerson(it) diff --git a/library/src/main/java/io/karn/notify/internal/RawNotification.kt b/library/src/main/java/io/karn/notify/internal/RawNotification.kt index f0f3b21..eb196b9 100644 --- a/library/src/main/java/io/karn/notify/internal/RawNotification.kt +++ b/library/src/main/java/io/karn/notify/internal/RawNotification.kt @@ -34,5 +34,6 @@ internal data class RawNotification( internal val content: Payload.Content, internal val bubblize: Payload.Bubble?, internal val stackable: Payload.Stackable?, - internal val actions: ArrayList? + internal val actions: ArrayList?, + internal val progress: Payload.Progress ) diff --git a/sample/src/main/java/presentation/MainActivity.kt b/sample/src/main/java/presentation/MainActivity.kt index 83025e6..b436950 100644 --- a/sample/src/main/java/presentation/MainActivity.kt +++ b/sample/src/main/java/presentation/MainActivity.kt @@ -155,4 +155,36 @@ class MainActivity : AppCompatActivity() { } .show() } + + fun notifyIndeterminateProgress(view: View) { + + Notify + .with(this) + .asBigText { + title = "Uploading files" + expandedText = "The files are being uploaded!" + bigText = "Daft Punk - Get Lucky.flac is uploading to server /music/favorites" + } + .progress { + showProgress = true + } + .show() + } + + fun notifyDeterminateProgress(view: View) { + + Notify + .with(this) + .asBigText { + title = "Bitcoin payment processing" + expandedText = "Your payment was sent to the Bitcoin network" + bigText = "Your payment #0489 is being confirmed 2/4" + } + .progress { + showProgress = true + enablePercentage = true + progressPercent = 30 + } + .show() + } } diff --git a/sample/src/main/res/layout/activity_main.xml b/sample/src/main/res/layout/activity_main.xml index 3fc40c8..5c3355e 100644 --- a/sample/src/main/res/layout/activity_main.xml +++ b/sample/src/main/res/layout/activity_main.xml @@ -79,4 +79,18 @@ android:layout_height="wrap_content" android:onClick="notifyBubble" android:text="Notify bubble!" /> + + + + From 247b9017e89ff8b9427a956a9b6d6b67727e5be6 Mon Sep 17 00:00:00 2001 From: Karn Saheb Date: Mon, 11 May 2020 22:25:42 -0400 Subject: [PATCH 7/9] Deprecate show(Int?) function in favour of show() --- .../src/main/java/io/karn/notify/Notify.kt | 4 ++-- .../main/java/io/karn/notify/NotifyCreator.kt | 23 ++++++++++++++++--- .../io/karn/notify/internal/utils/Utils.kt | 4 ++-- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/library/src/main/java/io/karn/notify/Notify.kt b/library/src/main/java/io/karn/notify/Notify.kt index d5c9351..c314b3b 100644 --- a/library/src/main/java/io/karn/notify/Notify.kt +++ b/library/src/main/java/io/karn/notify/Notify.kt @@ -112,7 +112,7 @@ class Notify internal constructor(internal var context: Context) { replaceWith = ReplaceWith("Notify.cancelNotification(context, id)")) @Throws(NullPointerException::class) fun cancelNotification(id: Int) { - return NotificationInterop.cancelNotification(Notify.defaultConfig.notificationManager!!, id) + return NotificationInterop.cancelNotification(defaultConfig.notificationManager!!, id) } /** @@ -159,6 +159,6 @@ class Notify internal constructor(internal var context: Context) { * this returned integer to make updates or to cancel the notification. */ internal fun show(id: Int?, builder: NotificationCompat.Builder): Int { - return NotificationInterop.showNotification(Notify.defaultConfig.notificationManager!!, id, builder) + return NotificationInterop.showNotification(defaultConfig.notificationManager!!, id, builder) } } diff --git a/library/src/main/java/io/karn/notify/NotifyCreator.kt b/library/src/main/java/io/karn/notify/NotifyCreator.kt index 1ea9d5c..abfd856 100644 --- a/library/src/main/java/io/karn/notify/NotifyCreator.kt +++ b/library/src/main/java/io/karn/notify/NotifyCreator.kt @@ -198,8 +198,8 @@ class NotifyCreator internal constructor(private val notify: Notify) { } /** - * Delegate a {@see Notification.Builder} object to the Notify NotificationInterop class which - * builds and displays the notification. + * Delegate a {@see Notification.Builder} object to the NotificationInterop class which builds + * and displays the notification. * * This is a terminal operation. * @@ -209,10 +209,27 @@ class NotifyCreator internal constructor(private val notify: Notify) { * @return An integer corresponding to the ID of the system notification. Any updates should use * this returned integer to make updates or to cancel the notification. */ - fun show(id: Int? = null): Int { + @Deprecated(message = "Removed optional argument to alleviate confusion on ID that is used to create notification", + replaceWith = ReplaceWith( + "Notify.show()", + "io.karn.notify.Notify")) + fun show(id: Int?): Int { return notify.show(id, asBuilder()) } + /** + * Delegate a @see{ Notification.Builder} object to the NotificationInterop class which builds + * and displays the notification. + * + * This is a terminal operation. + * + * @return An integer corresponding to the ID of the system notification. Any updates should use + * this returned integer to make updates or to cancel the notification. + */ + fun show(): Int { + return notify.show(null, asBuilder()) + } + /** * Cancel an existing notification given an ID. * diff --git a/library/src/main/java/io/karn/notify/internal/utils/Utils.kt b/library/src/main/java/io/karn/notify/internal/utils/Utils.kt index 62b341c..e10e990 100644 --- a/library/src/main/java/io/karn/notify/internal/utils/Utils.kt +++ b/library/src/main/java/io/karn/notify/internal/utils/Utils.kt @@ -25,11 +25,11 @@ package io.karn.notify.internal.utils import android.text.Html -import java.util.* +import java.util.Random internal object Utils { fun getRandomInt(): Int { - return Random().nextInt(Int.MAX_VALUE - 100) + 100 + return Random(System.currentTimeMillis()).nextInt() } fun getAsSecondaryFormattedText(str: String?): CharSequence? { From 0b5d56e3fbfb68770953132b4c829eecbb600579 Mon Sep 17 00:00:00 2001 From: Karn Saheb Date: Sun, 19 Jul 2020 23:06:58 -0400 Subject: [PATCH 8/9] Add support for Group Keys and Badge configuration (#68) * Add support for Group Keys and Badge config * Add test cases * Documentation + remove unused variable --- .../java/io/karn/notify/entities/Payload.kt | 13 ++++- .../internal/NotificationChannelInterop.kt | 2 + .../notify/internal/NotificationInterop.kt | 6 +-- .../java/io/karn/notify/NotifyAlertingTest.kt | 6 +++ .../java/io/karn/notify/NotifyMetaTest.kt | 3 ++ .../main/java/presentation/MainActivity.kt | 48 ++++++++----------- 6 files changed, 46 insertions(+), 32 deletions(-) diff --git a/library/src/main/java/io/karn/notify/entities/Payload.kt b/library/src/main/java/io/karn/notify/entities/Payload.kt index 6fc911e..ebc12e8 100644 --- a/library/src/main/java/io/karn/notify/entities/Payload.kt +++ b/library/src/main/java/io/karn/notify/entities/Payload.kt @@ -24,10 +24,12 @@ package io.karn.notify.entities +import android.annotation.TargetApi import android.app.PendingIntent import android.graphics.Bitmap import android.media.RingtoneManager import android.net.Uri +import android.os.Build import androidx.annotation.ColorInt import androidx.annotation.DrawableRes import androidx.core.app.NotificationCompat @@ -65,6 +67,10 @@ sealed class Payload { * notification as required. */ var category: String? = null, + /** + * A string value by which the system with decide how to group messages. + */ + var group: String? = null, /** * Set whether or not this notification is only relevant to the current device. */ @@ -141,7 +147,12 @@ sealed class Payload { * A custom notification sound if any. This is only set on notifications with importance * that is at least [Notify.IMPORTANCE_NORMAL] or higher. */ - var sound: Uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) + var sound: Uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), + /** + * A Boolean that indicates whether a notification channel + */ + @TargetApi(Build.VERSION_CODES.O) + var showBadge: Boolean = true ) /** diff --git a/library/src/main/java/io/karn/notify/internal/NotificationChannelInterop.kt b/library/src/main/java/io/karn/notify/internal/NotificationChannelInterop.kt index 38d5eed..3f049fc 100644 --- a/library/src/main/java/io/karn/notify/internal/NotificationChannelInterop.kt +++ b/library/src/main/java/io/karn/notify/internal/NotificationChannelInterop.kt @@ -71,6 +71,8 @@ internal object NotificationChannelInterop { setSound(it, android.media.AudioAttributes.Builder().build()) } + setShowBadge(alerting.showBadge) + Unit } diff --git a/library/src/main/java/io/karn/notify/internal/NotificationInterop.kt b/library/src/main/java/io/karn/notify/internal/NotificationInterop.kt index 8f17cb9..bd0c0fc 100644 --- a/library/src/main/java/io/karn/notify/internal/NotificationInterop.kt +++ b/library/src/main/java/io/karn/notify/internal/NotificationInterop.kt @@ -105,9 +105,7 @@ internal object NotificationInterop { .setContentText(Utils.getAsSecondaryFormattedText(payload.stackable.summaryDescription?.invoke(lines.size))) // Attach the stack click handler. .setContentIntent(payload.stackable.clickIntent) - .extend( - NotifyExtender().setStacked(true) - ) + .extend(NotifyExtender().setStacked(true)) // Clear the current set of actions and re-apply the stackable actions. builder.mActions.clear() @@ -139,6 +137,8 @@ internal object NotificationInterop { // The category of the notification which allows android to prioritize the // notification as required. .setCategory(payload.meta.category) + // Set the key by which this notification will be grouped. + .setGroup(payload.meta.group) // Set whether or not this notification is only relevant to the current device. .setLocalOnly(payload.meta.localOnly) // Set whether this notification is sticky. diff --git a/library/src/test/java/io/karn/notify/NotifyAlertingTest.kt b/library/src/test/java/io/karn/notify/NotifyAlertingTest.kt index 6354e39..d9acf0e 100644 --- a/library/src/test/java/io/karn/notify/NotifyAlertingTest.kt +++ b/library/src/test/java/io/karn/notify/NotifyAlertingTest.kt @@ -27,6 +27,7 @@ package io.karn.notify import android.graphics.Color import android.media.RingtoneManager import android.os.Build +import android.provider.Settings import androidx.core.app.NotificationCompat import org.junit.After import org.junit.Assert @@ -87,6 +88,8 @@ class NotifyAlertingTest : NotifyTestBase() { Assert.assertEquals(testAlerting.channelImportance + 3, shadowChannel.importance) // Assert.assertEquals(testLightColor, shadowChannel.lightColor) Assert.assertNull(shadowChannel.vibrationPattern) + Assert.assertEquals(Settings.System.DEFAULT_NOTIFICATION_URI, shadowChannel.sound) + Assert.assertTrue(shadowChannel.canShowBadge()) Assert.assertEquals(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), shadowChannel.sound) } @@ -112,6 +115,7 @@ class NotifyAlertingTest : NotifyTestBase() { lightColor = testLightColor vibrationPattern = testVibrationPattern sound = testSound + // 'group' not available in N } .content { title = "New dessert menu" @@ -151,6 +155,7 @@ class NotifyAlertingTest : NotifyTestBase() { lightColor = testLightColor vibrationPattern = testVibrationPattern sound = testSound + showBadge = false } .content { title = "New dessert menu" @@ -167,5 +172,6 @@ class NotifyAlertingTest : NotifyTestBase() { // Assert.assertEquals(testLightColor, shadowChannel.lightColor) Assert.assertEquals(testVibrationPattern, shadowChannel.vibrationPattern.toList()) Assert.assertEquals(testSound, shadowChannel.sound) + Assert.assertFalse(shadowChannel.canShowBadge()) } } diff --git a/library/src/test/java/io/karn/notify/NotifyMetaTest.kt b/library/src/test/java/io/karn/notify/NotifyMetaTest.kt index 8dbf9cd..47bf711 100644 --- a/library/src/test/java/io/karn/notify/NotifyMetaTest.kt +++ b/library/src/test/java/io/karn/notify/NotifyMetaTest.kt @@ -60,6 +60,7 @@ class NotifyMetaTest : NotifyTestBase() { val testCancelOnClick = false val testCategory = NotificationCompat.CATEGORY_STATUS + val testGroup = "test_group" val testTimeout = 5000L val notification = Notify.with(this.context) @@ -68,6 +69,7 @@ class NotifyMetaTest : NotifyTestBase() { clearIntent = testClearIntent cancelOnClick = testCancelOnClick category = testCategory + group = testGroup timeout = testTimeout people { add("mailto:hello@test.com") @@ -84,6 +86,7 @@ class NotifyMetaTest : NotifyTestBase() { Assert.assertEquals(testClearIntent, notification.deleteIntent) Assert.assertEquals(testCancelOnClick, (notification.flags and NotificationCompat.FLAG_AUTO_CANCEL) != 0) Assert.assertEquals(testCategory, notification.category) + Assert.assertEquals(testGroup, notification.group) Assert.assertEquals(testTimeout, notification.timeoutAfter) Assert.assertEquals(1, notification.extras.getStringArrayList(Notification.EXTRA_PEOPLE_LIST)?.size ?: 0) diff --git a/sample/src/main/java/presentation/MainActivity.kt b/sample/src/main/java/presentation/MainActivity.kt index b436950..73b773b 100644 --- a/sample/src/main/java/presentation/MainActivity.kt +++ b/sample/src/main/java/presentation/MainActivity.kt @@ -56,8 +56,7 @@ class MainActivity : AppCompatActivity() { } fun notifyDefault(view: View) { - Notify - .with(this) + Notify.with(this) .content { title = "New dessert menu" text = "The Cheesecake Factory has a new dessert for you to try!" @@ -72,8 +71,7 @@ class MainActivity : AppCompatActivity() { } fun notifyTextList(view: View) { - Notify - .with(this) + Notify.with(this) .asTextList { lines = Arrays.asList("New! Fresh Strawberry Cheesecake.", "New! Salted Caramel Cheesecake.", @@ -86,8 +84,7 @@ class MainActivity : AppCompatActivity() { } fun notifyBigText(view: View) { - Notify - .with(this) + Notify.with(this) .asBigText { title = "Chocolate brownie sundae" text = "Try our newest dessert option!" @@ -100,8 +97,7 @@ class MainActivity : AppCompatActivity() { } fun notifyBigPicture(view: View) { - Notify - .with(this) + Notify.with(this) .asBigPicture { title = "Chocolate brownie sundae" text = "Get a look at this amazing dessert!" @@ -112,8 +108,7 @@ class MainActivity : AppCompatActivity() { } fun notifyMessage(view: View) { - Notify - .with(this) + Notify.with(this) .asMessage { userDisplayName = "Karn" conversationTitle = "Sundae chat" @@ -138,8 +133,7 @@ class MainActivity : AppCompatActivity() { return } - Notify - .with(this) + Notify.with(this) .content { title = "New dessert menu" text = "The Cheesecake Factory has a new dessert for you to try!" @@ -158,9 +152,8 @@ class MainActivity : AppCompatActivity() { fun notifyIndeterminateProgress(view: View) { - Notify - .with(this) - .asBigText { + Notify.with(this) + .asBigText { title = "Uploading files" expandedText = "The files are being uploaded!" bigText = "Daft Punk - Get Lucky.flac is uploading to server /music/favorites" @@ -173,18 +166,17 @@ class MainActivity : AppCompatActivity() { fun notifyDeterminateProgress(view: View) { - Notify - .with(this) - .asBigText { - title = "Bitcoin payment processing" - expandedText = "Your payment was sent to the Bitcoin network" - bigText = "Your payment #0489 is being confirmed 2/4" - } - .progress { - showProgress = true - enablePercentage = true - progressPercent = 30 - } - .show() + Notify.with(this) + .asBigText { + title = "Bitcoin payment processing" + expandedText = "Your payment was sent to the Bitcoin network" + bigText = "Your payment #0489 is being confirmed 2/4" + } + .progress { + showProgress = true + enablePercentage = true + progressPercent = 30 + } + .show() } } From f7ec69e7875bb76b8ea95b6e78feb508067b84eb Mon Sep 17 00:00:00 2001 From: Karn Saheb Date: Sat, 13 Feb 2021 17:10:46 -0500 Subject: [PATCH 9/9] Version bump: 1.4.0 --- gradle/configuration.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/configuration.gradle b/gradle/configuration.gradle index a828c83..c79f238 100644 --- a/gradle/configuration.gradle +++ b/gradle/configuration.gradle @@ -7,8 +7,8 @@ def versions = [ - libCode: 12, - libName: '1.3.0', + libCode: 13, + libName: '1.4.0', kotlin: '1.3.11', core: '1.2.0-alpha04',