diff --git a/README.md b/README.md index bf8b2c9..b2419f4 100644 --- a/README.md +++ b/README.md @@ -13,9 +13,9 @@ Simplified notification delivery for Android. #### GETTING STARTED -You can install Notify using Jitpack while it is still in development. +Notify (pre-)releases are available via JitPack. It is recommended that a specific release version is selected when using the library in production as there may be breaking changes at anytime. -As such there currently are pre-releases available until test coverage is improved. +> **Tip:** Test out the canary channel to try out features by using the latest develop snapshot; `develop-SNAPSHOT`. ``` Groovy // Project level build.gradle @@ -27,8 +27,8 @@ repositories { // Module level build.gradle dependencies { - // -SNAPSHOT (latest release) - implementation "io.karn:notify:-SNAPSHOT" + // Replace version with release version, e.g. 1.0.0-alpha, -SNAPSHOT + implementation "io.karn:notify:[VERSION]" } ``` @@ -48,7 +48,11 @@ Notify ![Basic usecase](./docs/assets/default.svg) -If you run into a case in which the library does not provide the requisite builder functions you can get the `NotificationCompat.Builder` object and continue to use it as you would normally by calling `Creator#asBuilder()`. +If you run into a case in which the library does not provide the requisite builder functions you can get the `NotificationCompat.Builder` object and continue to use it as you would normally by calling `NotifyCreator#asBuilder()`. + +> **Tip:** You can view other notification styles on the [Notification Types](./docs/types.md) docs page. + +> **Tip:** Advanced usage topics are documented [here](./docs/advanced.md). #### NOTIFICATION ANATOMY @@ -61,8 +65,8 @@ If you run into a case in which the library does not provide the requisite build | 3 | Header Text | Optional description text. Set using the `Header#headerText` field. | | 4 | Timestamp | Timestamp of the notification. | | 5 | Expand Icon | Indicates that the notification is expandable. | -| 6 | Content | The "meat" of the notification set using of of the `Creator#as[Type]((Type) -> Unit)` scoped functions. | -| 7 | Actions | Set using the `Creator#actions((ArrayList) -> Unit)` scoped function. | +| 6 | Content | The "meat" of the notification set using of of the `NotifyCreator#as[Type]((Type) -> Unit)` scoped functions. | +| 7 | Actions | Set using the `NotifyCreator#actions((ArrayList) -> Unit)` scoped function. | #### CONTRIBUTING There are many ways to [contribute](./.github/CONTRIBUTING.md), you can diff --git a/build.gradle b/build.gradle index 270f66a..90a6dce 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.2.0-alpha07' + classpath 'com.android.tools.build:gradle:3.2.0-alpha14' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong @@ -39,5 +39,5 @@ allprojects { } task wrapper(type: Wrapper) { - gradleVersion = '4.5' + gradleVersion = '4.6' } diff --git a/docs/advanced.md b/docs/advanced.md new file mode 100644 index 0000000..4909f60 --- /dev/null +++ b/docs/advanced.md @@ -0,0 +1,10 @@ +## Advanced Usage +> **Note:** This page is still a work-in-progress. You can help complete the documentation by contributing to the project. + + +#### RESPONDING TO CLICKS +The `Payload.Meta` object provides `clickIntent` and `clearIntent` members which when not `null` will be fired when clicked or dismissed. + +#### STACKABLE NOTIFICATIONS + +#### ACTIONS \ No newline at end of file diff --git a/docs/assets/types/big-picture.svg b/docs/assets/types/big-picture.svg new file mode 100644 index 0000000..d63f369 --- /dev/null +++ b/docs/assets/types/big-picture.svg @@ -0,0 +1,74 @@ + + + + type/big-picture + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/assets/types/big-text.svg b/docs/assets/types/big-text.svg new file mode 100644 index 0000000..47a44a2 --- /dev/null +++ b/docs/assets/types/big-text.svg @@ -0,0 +1,71 @@ + + + + type/big-text + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/assets/types/default.svg b/docs/assets/types/default.svg new file mode 100644 index 0000000..c266bd9 --- /dev/null +++ b/docs/assets/types/default.svg @@ -0,0 +1,37 @@ + + + + type/default + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/assets/types/list-text.svg b/docs/assets/types/list-text.svg new file mode 100644 index 0000000..4ba366b --- /dev/null +++ b/docs/assets/types/list-text.svg @@ -0,0 +1,69 @@ + + + + type/list-text + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/assets/types/messages.svg b/docs/assets/types/messages.svg new file mode 100644 index 0000000..b2e91b3 --- /dev/null +++ b/docs/assets/types/messages.svg @@ -0,0 +1,78 @@ + + + + type/messages + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/types.md b/docs/types.md new file mode 100644 index 0000000..14fcb3a --- /dev/null +++ b/docs/types.md @@ -0,0 +1,120 @@ +## Notification Types + +#### STANDARD NOTIFICATION + +![Standard Notification](./assets/types/default.svg) + +Simple notifications are very easy! + +```Kotlin +Notify + .with(context) + .content { // this: Payload.Content.Default + // The title of the notification (first line). + title = "New dessert menu" + // The second line of the notification. + text = "The Cheesecake Factory has a new dessert for you to try!" + } + .show() +``` + +#### LIST NOTIFICATION + +![List Notification](./assets/types/list-text.svg) + +Notifications with multiple lines are very common. Notify provides a simple DSL to build these notfications. + +```Kotlin +Notify + .with(context) + .asTextList { // this: Payload.Content.TextList + // The lines that are shown when the notification is expanded. + lines = Arrays.asList("New! Fresh Strawberry Cheesecake.", + "New! Salted Caramel Cheesecake.", + "New! OREO Dream Dessert.") + // The title of the collapsed notification. + title = "New menu items!" + // The second line of the collapsed notification. + text = lines.size.toString() + " new dessert menu items found." + } + .show() +``` + +#### BIG TEXT + +![Big Text Notification](./assets/types/big-text.svg) + +For instances where you'd like to show a longer message you can use the `BigText` notification type. These kinds of messages are ideal for things such as email content, and news previews. + +```Kotlin +Notify + .with(context) + .asBigText { // this: Payload.Content.TextList + // The title of the notification. + title = "Chocolate brownie sundae" + // The second line of the (collapsed) notification. + text = "Try our newest dessert option!" + // The second line of the expanded notification. + expandedText = "Try our newest dessert option!" + // Large string that is displayed under the line above. + bigText = "Our own Fabulous Godiva Chocolate Brownie, Vanilla " + + "Ice Cream, Hot Fudge, Whipped Cream and Toasted " + + "Almonds.\n\n" + + "Come try this delicious new dessert and get two for " + + "the price of one!" + } + .show() +``` + +#### BIG PICTURE + +![Big Picture Notification](./assets/types/big-picture.svg) + +The big picture allows an application to notify the user in a manner similar to the [screenshot saved](https://www.androidexplained.com/wp-content/uploads/2017/10/Pixel-2-Screenshot-Notification.png) notification which shows a preview of the screenshot within the main content of the notification. + +```Kotlin +Notify + .with(context) + .asBigPicture { + // The title of the notification. + title = "Chocolate brownie sundae" + // The second line of the (collapsed) notification. + text = "Get a look at this amazing dessert!" + // The second line of the expanded notification. + expandedText = "The delicious brownie sundae now available." + // A bitmap that is to be shown. The system will automatically resize + // the image. + image = BitmapFactory.decodeResource(context.resources, + R.drawable.chocolate_brownie_sundae) + } + .show() +``` + +#### MESSAGE NOTIFICATION + +![Messages notification](./assets/types/messages.svg) + +The message notification is useful when displaying conversations within an application. It can also be useful to set the `headerText` field of the `Header` block with the number of messages outside the scope (list.size - 6). + +```Kotlin +Notify + .with(context) + .asMessage { + userDisplayName = "Karn" + conversationTitle = "Sundae chat" + messages = Arrays.asList( + NotificationCompat.MessagingStyle.Message( + "Are you guys ready to try the Strawberry sundae?", + System.currentTimeMillis() - (6 * 60 * 1000), // 6 Mins ago + "Karn"), + NotificationCompat.MessagingStyle.Message( + "Yeah! I've heard great things about this place.", + System.currentTimeMillis() - (5 * 60 * 1000), // 5 Mins ago + "Nitish"), + NotificationCompat.MessagingStyle.Message("What time are you getting there Karn?", + System.currentTimeMillis() - (1 * 60 * 1000), // 1 Mins ago + "Moez") + ) + } + .show() +``` \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 02be7ad..bf3de21 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Wed Apr 25 23:19:53 EDT 2018 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.5-all.zip diff --git a/library/src/main/java/io/karn/notify/Notify.kt b/library/src/main/java/io/karn/notify/Notify.kt index 7a4c0d6..f3efcdf 100644 --- a/library/src/main/java/io/karn/notify/Notify.kt +++ b/library/src/main/java/io/karn/notify/Notify.kt @@ -4,7 +4,9 @@ import android.app.NotificationManager import android.content.Context import android.support.v4.app.NotificationCompat import io.karn.notify.entities.NotifyConfig -import io.karn.notify.entities.RawNotification +import io.karn.notify.internal.RawNotification +import io.karn.notify.internal.NotificationChannelInterop +import io.karn.notify.internal.NotificationInterop /** * Simplified Notification delivery for Android. @@ -15,17 +17,47 @@ class Notify internal constructor(internal var context: Context) { /** * The default CHANNEL_ID for a notification on Android O. */ - const val DEFAULT_CHANNEL_KEY = "application_notification" + const val CHANNEL_DEFAULT_KEY = "application_notification" /** * The default CHANNEL_NAME for a notification on Android O. */ - const val DEFAULT_CHANNEL_NAME = "Application notifications." + const val CHANNEL_DEFAULT_NAME = "Application notifications." /** * The default CHANNEL_DESCRIPTION for a notification on Android O. */ - const val DEFAULT_CHANNEL_DESCRIPTION = "General application notifications." + const val CHANNEL_DEFAULT_DESCRIPTION = "General application notifications." + /** + * Lowest priority for a notification. These notifications might not be shown to the user except under special + * circumstances, such as detailed notification logs. + */ + const val IMPORTANCE_MIN = NotificationCompat.PRIORITY_MIN + /** + * Lower priority for notifications that are deemed less important. The UI may choose to show these items + * smaller, or at a different position in the list, compared to notifications with normal importance. + */ + const val IMPORTANCE_LOW = NotificationCompat.PRIORITY_LOW + /** + * Default priority for notifications. If your application does not prioritize its own notifications, use this + * value for all notifications. + */ + const val IMPORTANCE_NORMAL = NotificationCompat.PRIORITY_DEFAULT + /** + * Higher priority for notifications, for more important notifications or alerts. The UI may choose to show + * these items larger, or at a different position in notification lists, compared with your app's notifications + * of normal importance. + */ + const val IMPORTANCE_HIGH = NotificationCompat.PRIORITY_HIGH + /** + * Highest priority for notifications, use for notifications that require the user's prompt attention or input. + */ + const val IMPORTANCE_MAX = NotificationCompat.PRIORITY_MAX + + /** + * The flag to disable notification lights. + */ + const val NO_LIGHTS = 0 - // This is the initial configuration of the Notify Creator. + // This is the initial configuration of the Notify NotifyCreator. internal var defaultConfig = NotifyConfig() /** @@ -33,18 +65,18 @@ class Notify internal constructor(internal var context: Context) { * * Takes a receiver with the NotifyConfig immutable object which has mutable fields. */ - fun defaultConfig(block: (NotifyConfig) -> Unit) { - block(defaultConfig) + fun defaultConfig(init: NotifyConfig.() -> Unit) { + defaultConfig.init() } /** - * A new {@see Notify} and {@see Creator} instance. + * A new {@see Notify} and {@see NotifyCreator} instance. * - * This object is automatically initialized with the singleton default configuration which - * can be modified using {@see Notify#defaultConfig((NotifyConfig) -> Unit)}. + * This object is automatically initialized with the singleton default configuration which can be modified using + * {@see Notify#defaultConfig((NotifyConfig) -> Unit)}. */ - fun with(context: Context): Creator { - return Creator(Notify(context), defaultConfig) + fun with(context: Context): NotifyCreator { + return NotifyCreator(Notify(context), defaultConfig) } } @@ -52,33 +84,29 @@ class Notify internal constructor(internal var context: Context) { this.context = context.applicationContext // Initialize notification manager instance. - if (Companion.defaultConfig.notificationManager == null) { - Companion.defaultConfig.notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + if (defaultConfig.notificationManager == null) { + defaultConfig.notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager } - NotifyChannel.registerChannel( - Companion.defaultConfig.notificationManager!!, - defaultConfig.defaultChannelKey, - defaultConfig.defaultChannelName, - defaultConfig.defaultChannelDescription) + NotificationChannelInterop.with(defaultConfig.defaultAlerting) } /** - * Return the standard {@see NotificationCompat.Builder} after applying fluent API - * transformations (if any) from the {@see Creator} builder object. + * Return the standard {@see NotificationCompat.Builder} after applying fluent API transformations (if any) from the + * {@see NotifyCreator} builder object. */ internal fun asBuilder(payload: RawNotification): NotificationCompat.Builder { return NotificationInterop.buildNotification(this, payload) } /** - * 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 Notify 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. + * @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. */ internal fun show(builder: NotificationCompat.Builder): Int { return NotificationInterop.showNotification(Notify.defaultConfig.notificationManager!!, builder) diff --git a/library/src/main/java/io/karn/notify/NotifyChannel.kt b/library/src/main/java/io/karn/notify/NotifyChannel.kt deleted file mode 100644 index 7e019f1..0000000 --- a/library/src/main/java/io/karn/notify/NotifyChannel.kt +++ /dev/null @@ -1,27 +0,0 @@ -package io.karn.notify - -import android.app.NotificationChannel -import android.app.NotificationManager -import android.os.Build - -/** - * Provides compatibility functionality for the Notification channels introduced in Android O. - */ -internal object NotifyChannel { - - fun registerChannel(notificationManager: NotificationManager, channelKey: String, channelName: String, channelDescription: String, importance: Int = NotificationManager.IMPORTANCE_DEFAULT): Boolean { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { - return false - } - - // Create the NotificationChannel, but only on API 26+ because - // the NotificationChannel class is new and not in the support library - val channel = NotificationChannel(channelKey, channelName, importance) - - channel.description = channelDescription - // Register the channel with the system - notificationManager.createNotificationChannel(channel) - - return true - } -} diff --git a/library/src/main/java/io/karn/notify/Creator.kt b/library/src/main/java/io/karn/notify/NotifyCreator.kt similarity index 71% rename from library/src/main/java/io/karn/notify/Creator.kt rename to library/src/main/java/io/karn/notify/NotifyCreator.kt index f9cf326..7757bab 100644 --- a/library/src/main/java/io/karn/notify/Creator.kt +++ b/library/src/main/java/io/karn/notify/NotifyCreator.kt @@ -3,20 +3,20 @@ package io.karn.notify import android.support.v4.app.NotificationCompat import io.karn.notify.entities.NotifyConfig import io.karn.notify.entities.Payload -import io.karn.notify.entities.RawNotification -import io.karn.notify.utils.Action -import io.karn.notify.utils.Errors -import io.karn.notify.utils.NotifyScopeMarker +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.NotifyScopeMarker /** * Fluent API for creating a Notification object. */ @NotifyScopeMarker -class Creator internal constructor(private val notify: Notify, config: NotifyConfig = NotifyConfig()) { +class NotifyCreator internal constructor(private val notify: Notify, config: NotifyConfig = NotifyConfig()) { private var meta = Payload.Meta() - private var alerts = Payload.Alerts() - private var header = config.header.copy() + private var alerts = config.defaultAlerting + private var header = config.defaultHeader.copy() private var content: Payload.Content = Payload.Content.Default() private var actions: ArrayList? = null private var stackable: Payload.Stackable? = null @@ -25,7 +25,7 @@ class Creator internal constructor(private val notify: Notify, config: NotifyCon * Scoped function for modifying the Metadata of a notification, such as click intents, * notification category, and priority among other options. */ - fun meta(init: Payload.Meta.() -> Unit): Creator { + fun meta(init: Payload.Meta.() -> Unit): NotifyCreator { this.meta.init() return this @@ -34,28 +34,31 @@ class Creator internal constructor(private val notify: Notify, config: NotifyCon /** * Scoped function for modifying the Alerting of a notification. This includes visibility, * sounds, lights, etc. + * + * If an existing key is provided the existing channel is retrieved (API >= AndroidO) and set as the alerting + * configuration. If the key is new, the channel is created and set as the alerting configuration. */ - fun alerting(init: Payload.Alerts.() -> Unit): Creator { + 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() - return this } /** * Scoped function for modifying the Header of a notification. Specifically, it allows the * modification of the notificationIcon, color, the headerText (optional text next to the - * appName), and finally the channel of the notification if targeting Android O. + * appName), and finally the notifyChannel of the notification if targeting Android O. */ - fun header(init: Payload.Header.() -> Unit): Creator { + fun header(init: Payload.Header.() -> Unit): NotifyCreator { this.header.init() - return this } /** * Scoped function for modifying the content of a 'Default' notification. */ - fun content(init: Payload.Content.Default.() -> Unit): Creator { + fun content(init: Payload.Content.Default.() -> Unit): NotifyCreator { this.content = Payload.Content.Default() (this.content as Payload.Content.Default).init() return this @@ -64,7 +67,7 @@ class Creator internal constructor(private val notify: Notify, config: NotifyCon /** * Scoped function for modifying the content of a 'TextList' notification. */ - fun asTextList(init: Payload.Content.TextList.() -> Unit): Creator { + fun asTextList(init: Payload.Content.TextList.() -> Unit): NotifyCreator { this.content = Payload.Content.TextList() (this.content as Payload.Content.TextList).init() return this @@ -73,7 +76,7 @@ class Creator internal constructor(private val notify: Notify, config: NotifyCon /** * Scoped function for modifying the content of a 'BigText' notification. */ - fun asBigText(init: Payload.Content.BigText.() -> Unit): Creator { + fun asBigText(init: Payload.Content.BigText.() -> Unit): NotifyCreator { this.content = Payload.Content.BigText() (this.content as Payload.Content.BigText).init() return this @@ -82,7 +85,7 @@ class Creator internal constructor(private val notify: Notify, config: NotifyCon /** * Scoped function for modifying the content of a 'BigPicture' notification. */ - fun asBigPicture(init: Payload.Content.BigPicture.() -> Unit): Creator { + fun asBigPicture(init: Payload.Content.BigPicture.() -> Unit): NotifyCreator { this.content = Payload.Content.BigPicture() (this.content as Payload.Content.BigPicture).init() return this @@ -91,7 +94,7 @@ class Creator internal constructor(private val notify: Notify, config: NotifyCon /** * Scoped function for modifying the content of a 'Message' notification. */ - fun asMessage(init: Payload.Content.Message.() -> Unit): Creator { + fun asMessage(init: Payload.Content.Message.() -> Unit): NotifyCreator { this.content = Payload.Content.Message() (this.content as Payload.Content.Message).init() return this @@ -101,7 +104,7 @@ class Creator internal constructor(private val notify: Notify, config: NotifyCon * Scoped function for modifying the 'Actions' of a notification. The transformation * relies on adding standard notification Action objects. */ - fun actions(init: ArrayList.() -> Unit): Creator { + fun actions(init: ArrayList.() -> Unit): NotifyCreator { this.actions = ArrayList() (this.actions as ArrayList).init() return this @@ -111,7 +114,7 @@ class Creator internal constructor(private val notify: Notify, config: NotifyCon * Scoped function for modifying the behaviour of 'Stacked' notifications. The transformation * relies on the 'summaryText' of a stackable notification. */ - fun stackable(init: Payload.Stackable.() -> Unit): Creator { + fun stackable(init: Payload.Stackable.() -> Unit): NotifyCreator { this.stackable = Payload.Stackable() (this.stackable as Payload.Stackable).init() @@ -126,7 +129,7 @@ class Creator internal constructor(private val notify: Notify, config: NotifyCon /** * Return the standard {@see NotificationCompat.Builder} after applying fluent API - * transformations (if any) from the {@see Creator} builder object. + * transformations (if any) from the {@see NotifyCreator} builder object. */ fun asBuilder(): NotificationCompat.Builder { return notify.asBuilder(RawNotification(meta, alerts, header, content, stackable, actions)) 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 6709b62..e062737 100644 --- a/library/src/main/java/io/karn/notify/entities/NotifyConfig.kt +++ b/library/src/main/java/io/karn/notify/entities/NotifyConfig.kt @@ -1,34 +1,34 @@ package io.karn.notify.entities -import android.annotation.TargetApi import android.app.NotificationManager -import android.os.Build -import android.support.v4.app.NotificationManagerCompat -import io.karn.notify.Notify /** - * Provider of the initial configuration of the Notify > Creator Fluent API. + * Provider of the initial configuration of the Notify > NotifyCreator Fluent API. */ data class NotifyConfig( /** - * The default CHANNEL_ID for a notification on Android O. - */ - @TargetApi(Build.VERSION_CODES.O) val defaultChannelKey: String = Notify.DEFAULT_CHANNEL_KEY, - /** - * The default CHANNEL_NAME for a notification on Android O. - */ - @TargetApi(Build.VERSION_CODES.O) val defaultChannelName: String = Notify.DEFAULT_CHANNEL_NAME, - /** - * The default CHANNEL_DESCRIPTION for a notification on Android O. + * A reference to the notification manager. */ - @TargetApi(Build.VERSION_CODES.O) val defaultChannelDescription: String = Notify.DEFAULT_CHANNEL_DESCRIPTION, + internal var notificationManager: NotificationManager? = null, /** * Specifies the default configuration of a notification (e.g the default notificationIcon, * and notification color.) */ - @TargetApi(Build.VERSION_CODES.O) val header: Payload.Header = Payload.Header(channel = defaultChannelKey), + internal var defaultHeader: Payload.Header = Payload.Header(), /** - * A reference to the notification manager. + * Specifies the default alerting configuration for notifications. */ - internal var notificationManager: NotificationManager? = null -) + internal var defaultAlerting: Payload.Alerts = Payload.Alerts() +) { + fun header(init: Payload.Header.() -> Unit): NotifyConfig { + defaultHeader.init() + return this + } + + fun alerting(key: String, init: Payload.Alerts.() -> Unit): NotifyConfig { + // Clone object and assign the key. + defaultAlerting = defaultAlerting.copy(channelKey = key) + defaultAlerting.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 39bad2b..e51bb9a 100644 --- a/library/src/main/java/io/karn/notify/entities/Payload.kt +++ b/library/src/main/java/io/karn/notify/entities/Payload.kt @@ -1,15 +1,17 @@ package io.karn.notify.entities -import android.annotation.TargetApi import android.app.PendingIntent import android.graphics.Bitmap -import android.os.Build +import android.media.RingtoneManager +import android.net.Uri +import android.support.annotation.ColorInt import android.support.annotation.ColorRes import android.support.annotation.DrawableRes import android.support.v4.app.NotificationCompat +import io.karn.notify.Notify import io.karn.notify.R -import io.karn.notify.utils.Action -import java.util.* +import io.karn.notify.internal.utils.Action +import io.karn.notify.internal.utils.NotifyImportance /** * Wrapper class to provide configurable options for a NotifcationCompact object. @@ -39,10 +41,6 @@ sealed class Payload { * notification as required. */ var category: String? = null, - /** - * Manual specification of the priority of the notification. - */ - var priority: Int = NotificationCompat.PRIORITY_DEFAULT, /** * Set whether or not this notification is only relevant to the current device. */ @@ -51,8 +49,31 @@ sealed class Payload { * Indicates whether the notification is sticky. If enabled, the notification is not * affected by the clear all and is not dismissible. */ - var sticky: Boolean = false - ) + var sticky: Boolean = false, + /** + * The duration of time in milliseconds after which the notification is automatically dismissed. + */ + var timeout: Long = 0L, + /** + * Add a person that is relevant to this notification. + * + * Depending on user preferences, this may allow the notification to pass through interruption filters, and + * to appear more prominently in the user interface. + * + * The person should be specified by the {@code String} representation of a + * {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI}. + * + * The system will also attempt to resolve {@code mailto:} and {@code tel:} schema + * URIs. The path part of these URIs must exist in the contacts database, in the + * appropriate column, or the reference will be discarded as invalid. Telephone schema + * URIs will be resolved by {@link android.provider.ContactsContract.PhoneLookup}. + */ + internal val contacts: ArrayList = ArrayList() + ) { + fun people(init: ArrayList.() -> Unit) { + contacts.init() + } + } /** * Defines the alerting configuration for a particular notification. This includes notification @@ -68,9 +89,33 @@ sealed class Payload { */ @NotificationCompat.NotificationVisibility var lockScreenVisibility: Int = NotificationCompat.VISIBILITY_PRIVATE, /** - * The duration of time in milliseconds after which the notification is automatically dismissed. + * The default CHANNEL_ID for a notification on versions >= Android O. + */ + val channelKey: String = Notify.CHANNEL_DEFAULT_KEY, + /** + * The default CHANNEL_NAME for a notification on versions >= Android O. */ - var timeout: Long = 0L + var channelName: String = Notify.CHANNEL_DEFAULT_NAME, + /** + * The default CHANNEL_DESCRIPTION for a notification on versions >= Android O. + */ + var channelDescription: String = Notify.CHANNEL_DEFAULT_DESCRIPTION, + /** + * The default IMPORTANCE for a notification. + */ + @NotifyImportance var channelImportance: Int = Notify.IMPORTANCE_NORMAL, + /** + * The LED colors of the notification notifyChannel. + */ + @ColorInt var lightColor: Int = Notify.NO_LIGHTS, + /** + * Vibration pattern for notification on this notifyChannel. + */ + var vibrationPattern: List = ArrayList(), + /** + * A custom notification sound if any. + */ + var sound: Uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) ) /** @@ -89,10 +134,6 @@ sealed class Payload { * The optional text that appears next to the appName of a notification. */ var headerText: CharSequence? = null, - /** - * Manual override of channel on which this notification is broadcasted. - */ - @TargetApi(Build.VERSION_CODES.O) var channel: String = "", /** * Setting this field to false results in the timestamp (now, 5m, ...) next to the * application name to be hidden. @@ -119,14 +160,21 @@ sealed class Payload { var text: CharSequence? } + interface SupportsLargeIcon { + /** + * The large icon of the notification. + */ + var largeIcon: Bitmap? + } + /** * Indicates whether a notification is expandable. */ interface Expandable { /** - * The content that is displayed when the notification is not expanded. + * The content that is displayed when the notification is expanded expanded. */ - var collapsedText: CharSequence? + var expandedText: CharSequence? } /** @@ -134,8 +182,9 @@ sealed class Payload { */ data class Default( override var title: CharSequence? = null, - override var text: CharSequence? = null - ) : Content(), Standard + override var text: CharSequence? = null, + override var largeIcon: Bitmap? = null + ) : Content(), Standard, SupportsLargeIcon /** * The object representation of a 'TextList' notification. @@ -143,11 +192,12 @@ sealed class Payload { data class TextList( override var title: CharSequence? = null, override var text: CharSequence? = null, + override var largeIcon: Bitmap? = null, /** * The lines of the notification. */ var lines: List = ArrayList() - ) : Content(), Standard + ) : Content(), Standard, SupportsLargeIcon /** * The object representation of a 'BigText' notification. @@ -155,12 +205,13 @@ sealed class Payload { data class BigText( override var title: CharSequence? = null, override var text: CharSequence? = null, - override var collapsedText: CharSequence? = null, + override var largeIcon: Bitmap? = null, + override var expandedText: CharSequence? = null, /** * The large text associated with the notification. */ var bigText: CharSequence? = null - ) : Content(), Standard, Expandable + ) : Content(), Standard, SupportsLargeIcon, Expandable /** * The object representation of a 'BigPicture' notification. @@ -168,17 +219,19 @@ sealed class Payload { data class BigPicture( override var title: CharSequence? = null, override var text: CharSequence? = null, - override var collapsedText: CharSequence? = null, + override var largeIcon: Bitmap? = null, + override var expandedText: CharSequence? = null, /** * The large image that appears when the notification is expanded.s */ var image: Bitmap? = null - ) : Content(), Standard, Expandable + ) : Content(), Standard, SupportsLargeIcon, Expandable /** * The object representaiton of a 'Message' notification. */ data class Message( + override var largeIcon: Bitmap? = null, /** * The title of the conversation. */ @@ -191,7 +244,7 @@ sealed class Payload { * A collection of messages associated with a particualar conversation. */ var messages: List = ArrayList() - ) : Content() + ) : Content(), SupportsLargeIcon } /** diff --git a/library/src/main/java/io/karn/notify/internal/NotificationChannelInterop.kt b/library/src/main/java/io/karn/notify/internal/NotificationChannelInterop.kt new file mode 100644 index 0000000..0c2eee2 --- /dev/null +++ b/library/src/main/java/io/karn/notify/internal/NotificationChannelInterop.kt @@ -0,0 +1,58 @@ +package io.karn.notify.internal + +import android.annotation.SuppressLint +import android.app.NotificationChannel +import android.os.Build +import io.karn.notify.Notify +import io.karn.notify.entities.Payload + +/** + * Provides compatibility functionality for the Notification channels introduced in Android O. + */ +internal object NotificationChannelInterop { + @SuppressLint("WrongConstant") + fun with(alerting: Payload.Alerts): Boolean { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + return false + } + + val notificationManager = Notify.defaultConfig.notificationManager!! + + // Ensure that the alerting is not already registered -- return true if it exists. + notificationManager.getNotificationChannel(alerting.channelKey)?.run { + return true + } + + // Create the NotificationChannel, but only on API 26+ because + // the NotificationChannel class is new and not in the support library + val channel = NotificationChannel(alerting.channelKey, alerting.channelName, alerting.channelImportance + 2).apply { + description = alerting.channelDescription + + // Set the lockscreen visibility. + lockscreenVisibility = alerting.lockScreenVisibility + + alerting.lightColor + .takeIf { it != Notify.NO_LIGHTS } + ?.let { + enableLights(true) + lightColor = alerting.lightColor + } + + alerting.vibrationPattern.takeIf { it.isNotEmpty() }?.also { + enableVibration(true) + vibrationPattern = it.toLongArray() + } + + alerting.sound.also { + setSound(it, android.media.AudioAttributes.Builder().build()) + } + + Unit + } + + // Register the alerting with the system + notificationManager.createNotificationChannel(channel) + + return true + } +} diff --git a/library/src/main/java/io/karn/notify/NotificationInterop.kt b/library/src/main/java/io/karn/notify/internal/NotificationInterop.kt similarity index 83% rename from library/src/main/java/io/karn/notify/NotificationInterop.kt rename to library/src/main/java/io/karn/notify/internal/NotificationInterop.kt index ff1b5fc..fd234b1 100644 --- a/library/src/main/java/io/karn/notify/NotificationInterop.kt +++ b/library/src/main/java/io/karn/notify/internal/NotificationInterop.kt @@ -1,13 +1,13 @@ -package io.karn.notify +package io.karn.notify.internal import android.app.NotificationManager import android.os.Build import android.support.annotation.VisibleForTesting import android.support.v4.app.NotificationCompat import android.text.Html +import io.karn.notify.Notify import io.karn.notify.entities.Payload -import io.karn.notify.entities.RawNotification -import io.karn.notify.utils.Utils +import io.karn.notify.internal.utils.Utils internal object NotificationInterop { @@ -95,7 +95,7 @@ internal object NotificationInterop { } fun buildNotification(notify: Notify, payload: RawNotification): NotificationCompat.Builder { - val builder = NotificationCompat.Builder(notify.context, payload.header.channel) + val builder = NotificationCompat.Builder(notify.context, payload.alerting.channelKey) // Ensures that this notification is marked as a Notify notification. .extend(NotifyExtender()) // The color of the RawNotification Icon, App_Name and the expanded chevron. @@ -115,16 +115,17 @@ internal object NotificationInterop { // The category of the notification which allows android to prioritize the // notification as required. .setCategory(payload.meta.category) - // Manual specification of the priority. - .setPriority(payload.meta.priority) // Set whether or not this notification is only relevant to the current device. .setLocalOnly(payload.meta.localOnly) // Set whether this notification is sticky. .setOngoing(payload.meta.sticky) - // The visibility of the notification on the lockscreen. - .setVisibility(payload.alerting.lockScreenVisibility) // The duration of time after which the notification is automatically dismissed. - .setTimeoutAfter(payload.alerting.timeout) + .setTimeoutAfter(payload.meta.timeout) + + // Add contacts if any -- will help display prominently if possible. + payload.meta.contacts.takeIf { it.isNotEmpty() }?.forEach { + builder.addPerson(it) + } // Standard notifications have the collapsed title and text. if (payload.content is Payload.Content.Standard) { @@ -134,11 +135,43 @@ internal object NotificationInterop { .setContentText(payload.content.text) } + if (payload.content is Payload.Content.SupportsLargeIcon) { + // Sets the large icon of the notification. + builder.setLargeIcon(payload.content.largeIcon) + } + // Attach all the actions. payload.actions?.forEach { builder.addAction(it) } + // Attach alerting options. + payload.alerting.apply { + // Register the default alerting. + NotificationChannelInterop.with(this) + + // The visibility of the notification on the lockscreen. + builder.setVisibility(lockScreenVisibility) + + // The lights of the notification. + if (lightColor != Notify.NO_LIGHTS) { + builder.setLights(lightColor, 500, 2000) + } + + // The vibration pattern. + vibrationPattern + .takeIf { it.isNotEmpty() } + ?.also { + builder.setVibrate(it.toLongArray()) + } + + // A custom alerting sound. + builder.setSound(sound) + + // Manual specification of the priority. + builder.priority = channelImportance + } + var style: NotificationCompat.Style? = null payload.stackable?.let { @@ -178,23 +211,19 @@ internal object NotificationInterop { builder.setContentText(Utils.getAsSecondaryFormattedText((content.text ?: "").toString())) - val bigText: CharSequence = Html.fromHtml("" + (content.collapsedText + val bigText: CharSequence = Html.fromHtml("" + (content.expandedText ?: content.title) + "
" + content.bigText?.replace("\n".toRegex(), "
")) NotificationCompat.BigTextStyle() .bigText(bigText) } is Payload.Content.BigPicture -> { - // Document these by linking to resource with labels. (1), (2), etc. - - // This large icon is show in both expanded and collapsed views. Might consider creating a custom view for this. - // builder.setLargeIcon(content.image) - NotificationCompat.BigPictureStyle() // This is the second line in the 'expanded' notification. - .setSummaryText(content.collapsedText ?: content.text) + .setSummaryText(content.expandedText ?: content.text) // This is the picture below. .bigPicture(content.image) + .bigLargeIcon(null) } is Payload.Content.Message -> { diff --git a/library/src/main/java/io/karn/notify/NotifyExtender.kt b/library/src/main/java/io/karn/notify/internal/NotifyExtender.kt similarity index 99% rename from library/src/main/java/io/karn/notify/NotifyExtender.kt rename to library/src/main/java/io/karn/notify/internal/NotifyExtender.kt index 8598f36..7b98744 100644 --- a/library/src/main/java/io/karn/notify/NotifyExtender.kt +++ b/library/src/main/java/io/karn/notify/internal/NotifyExtender.kt @@ -1,4 +1,4 @@ -package io.karn.notify +package io.karn.notify.internal import android.os.Bundle import android.service.notification.StatusBarNotification diff --git a/library/src/main/java/io/karn/notify/entities/RawNotification.kt b/library/src/main/java/io/karn/notify/internal/RawNotification.kt similarity index 73% rename from library/src/main/java/io/karn/notify/entities/RawNotification.kt rename to library/src/main/java/io/karn/notify/internal/RawNotification.kt index c11426c..574acb6 100644 --- a/library/src/main/java/io/karn/notify/entities/RawNotification.kt +++ b/library/src/main/java/io/karn/notify/internal/RawNotification.kt @@ -1,6 +1,7 @@ -package io.karn.notify.entities +package io.karn.notify.internal -import io.karn.notify.utils.Action +import io.karn.notify.entities.Payload +import io.karn.notify.internal.utils.Action internal data class RawNotification( internal val meta: Payload.Meta, diff --git a/library/src/main/java/io/karn/notify/utils/Aliases.kt b/library/src/main/java/io/karn/notify/internal/utils/Aliases.kt similarity index 71% rename from library/src/main/java/io/karn/notify/utils/Aliases.kt rename to library/src/main/java/io/karn/notify/internal/utils/Aliases.kt index 7b77073..1f6704f 100644 --- a/library/src/main/java/io/karn/notify/utils/Aliases.kt +++ b/library/src/main/java/io/karn/notify/internal/utils/Aliases.kt @@ -1,4 +1,4 @@ -package io.karn.notify.utils +package io.karn.notify.internal.utils import android.support.v4.app.NotificationCompat 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 new file mode 100644 index 0000000..83df29f --- /dev/null +++ b/library/src/main/java/io/karn/notify/internal/utils/Annotations.kt @@ -0,0 +1,15 @@ +package io.karn.notify.internal.utils + +import android.support.annotation.IntDef +import io.karn.notify.Notify + +@DslMarker +annotation class NotifyScopeMarker + +@Retention(AnnotationRetention.SOURCE) +@IntDef(Notify.IMPORTANCE_MIN, + Notify.IMPORTANCE_LOW, + Notify.IMPORTANCE_NORMAL, + Notify.IMPORTANCE_HIGH, + Notify.IMPORTANCE_MAX) +annotation class NotifyImportance diff --git a/library/src/main/java/io/karn/notify/utils/Errors.kt b/library/src/main/java/io/karn/notify/internal/utils/Errors.kt similarity index 72% rename from library/src/main/java/io/karn/notify/utils/Errors.kt rename to library/src/main/java/io/karn/notify/internal/utils/Errors.kt index 61f92dc..ae3f47b 100644 --- a/library/src/main/java/io/karn/notify/utils/Errors.kt +++ b/library/src/main/java/io/karn/notify/internal/utils/Errors.kt @@ -1,6 +1,5 @@ -package io.karn.notify.utils +package io.karn.notify.internal.utils internal object Errors { - const val INVALID_STACK_KEY_ERROR = "Invalid stack key provided." } diff --git a/library/src/main/java/io/karn/notify/utils/Utils.kt b/library/src/main/java/io/karn/notify/internal/utils/Utils.kt similarity index 89% rename from library/src/main/java/io/karn/notify/utils/Utils.kt rename to library/src/main/java/io/karn/notify/internal/utils/Utils.kt index 722ec48..2e26d38 100644 --- a/library/src/main/java/io/karn/notify/utils/Utils.kt +++ b/library/src/main/java/io/karn/notify/internal/utils/Utils.kt @@ -1,4 +1,4 @@ -package io.karn.notify.utils +package io.karn.notify.internal.utils import android.text.Html import java.util.* diff --git a/library/src/main/java/io/karn/notify/utils/Annotations.kt b/library/src/main/java/io/karn/notify/utils/Annotations.kt deleted file mode 100644 index 7d1f6f3..0000000 --- a/library/src/main/java/io/karn/notify/utils/Annotations.kt +++ /dev/null @@ -1,4 +0,0 @@ -package io.karn.notify.utils - -@DslMarker -annotation class NotifyScopeMarker diff --git a/library/src/test/java/io/karn/notify/NotificationChannelInteropTest.kt b/library/src/test/java/io/karn/notify/NotificationChannelInteropTest.kt new file mode 100644 index 0000000..db02ad3 --- /dev/null +++ b/library/src/test/java/io/karn/notify/NotificationChannelInteropTest.kt @@ -0,0 +1,41 @@ +package io.karn.notify + +import android.os.Build +import io.karn.notify.entities.Payload +import io.karn.notify.internal.NotificationChannelInterop +import org.junit.After +import org.junit.Assert +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.util.ReflectionHelpers + + +@RunWith(RobolectricTestRunner::class) +class NotificationChannelInteropTest : NotifyTestBase() { + + @After + fun runAfter() { + ReflectionHelpers.setStaticField(Build.VERSION::class.java, SDK_INT, currentSdkVersion) + } + + @Test + fun registerChannelTest_onAndroidN() { + ReflectionHelpers.setStaticField(Build.VERSION::class.java, SDK_INT, Build.VERSION_CODES.N_MR1) + + val registeredChannel = NotificationChannelInterop.with(Payload.Alerts()) + + Assert.assertFalse(registeredChannel) + } + + @Test + fun registerChannelTest_onAndroidO() { + ReflectionHelpers.setStaticField(Build.VERSION::class.java, SDK_INT, Build.VERSION_CODES.O) + + val testAlerting = Payload.Alerts() + val registeredChannel = NotificationChannelInterop.with(testAlerting) + + Assert.assertTrue(registeredChannel) + Assert.assertNotNull(shadowNotificationManager.getNotificationChannel(testAlerting.channelKey)) + } +} diff --git a/library/src/test/java/io/karn/notify/NotificationInterlopTest.kt b/library/src/test/java/io/karn/notify/NotificationInterlopTest.kt new file mode 100644 index 0000000..8b50b6a --- /dev/null +++ b/library/src/test/java/io/karn/notify/NotificationInterlopTest.kt @@ -0,0 +1,59 @@ +package io.karn.notify + +import android.os.Build +import android.support.v4.app.NotificationCompat +import io.karn.notify.internal.NotificationInterop +import junit.framework.Assert +import org.junit.After +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.util.ReflectionHelpers + +@RunWith(RobolectricTestRunner::class) +class NotificationInterlopTest : NotifyTestBase() { + + @After + fun runAfter() { + ReflectionHelpers.setStaticField(Build.VERSION::class.java, SDK_INT, currentSdkVersion) + } + + @Test + fun getActiveNotifications_onAndroidLollipop() { + ReflectionHelpers.setStaticField(Build.VERSION::class.java, SDK_INT, Build.VERSION_CODES.LOLLIPOP_MR1) + + Notify.with(this.context) + .content { + title = "New dessert menu" + text = "The Cheesecake Factory has a new dessert for you to try!" + } + .show() + + val notifications = NotificationInterop.getActiveNotifications(shadowNotificationManager) + Assert.assertEquals(0, notifications.size) + } + + @Test + fun getActiveNotifications_onAndroidM() { + ReflectionHelpers.setStaticField(Build.VERSION::class.java, SDK_INT, Build.VERSION_CODES.M) + + Notify.with(this.context) + .content { + title = "New dessert menu" + text = "The Cheesecake Factory has a new dessert for you to try!" + } + .show() + + val ogNotification = NotificationCompat.Builder(this.context, Notify.defaultConfig.defaultAlerting.channelKey) + .setContentTitle("Title") + .setContentText("Text") + .build() + + shadowNotificationManager.notify(123, ogNotification) + + val allNotfications = shadowNotificationManager.activeNotifications + Assert.assertEquals(2, allNotfications.size) + val notifyNotifications = NotificationInterop.getActiveNotifications(shadowNotificationManager) + Assert.assertEquals(1, notifyNotifications.size) + } +} diff --git a/library/src/test/java/io/karn/notify/NotifyActionsTest.kt b/library/src/test/java/io/karn/notify/NotifyActionsTest.kt index cfc7aeb..ca08405 100644 --- a/library/src/test/java/io/karn/notify/NotifyActionsTest.kt +++ b/library/src/test/java/io/karn/notify/NotifyActionsTest.kt @@ -1,6 +1,6 @@ package io.karn.notify -import io.karn.notify.utils.Action +import io.karn.notify.internal.utils.Action import org.junit.Assert import org.junit.Test import org.junit.runner.RunWith diff --git a/library/src/test/java/io/karn/notify/NotifyAlertingTest.kt b/library/src/test/java/io/karn/notify/NotifyAlertingTest.kt index 6b07ea2..1303dee 100644 --- a/library/src/test/java/io/karn/notify/NotifyAlertingTest.kt +++ b/library/src/test/java/io/karn/notify/NotifyAlertingTest.kt @@ -1,16 +1,30 @@ package io.karn.notify +import android.graphics.Color +import android.media.RingtoneManager +import android.os.Build import android.support.v4.app.NotificationCompat +import org.junit.After import org.junit.Assert import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +import org.robolectric.util.ReflectionHelpers @RunWith(RobolectricTestRunner::class) class NotifyAlertingTest : NotifyTestBase() { + @After + fun runAfter() { + ReflectionHelpers.setStaticField(Build.VERSION::class.java, SDK_INT, currentSdkVersion) + } + @Test - fun defaultAlertingTest() { + fun defaultAlertingTest_onAndroidN() { + ReflectionHelpers.setStaticField(Build.VERSION::class.java, SDK_INT, Build.VERSION_CODES.N_MR1) + + val testAlerting = Notify.defaultConfig.defaultAlerting + val notification = Notify.with(this.context) .content { title = "New dessert menu" @@ -19,19 +33,61 @@ class NotifyAlertingTest : NotifyTestBase() { .asBuilder() .build() - Assert.assertEquals(0, notification.visibility) - Assert.assertEquals(0, notification.timeoutAfter) + Assert.assertNull(notification.channelId) + Assert.assertEquals(testAlerting.lockScreenVisibility, notification.visibility) + Assert.assertEquals(testAlerting.channelImportance, notification.priority) + // Color comparison nonsense again. + // Assert.assertEquals(testLightColor, notification.color) + Assert.assertNull(notification.vibrate) + Assert.assertEquals(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), notification.sound) + } + + @Test + fun defaultAlertingTest_onAndroidO() { + ReflectionHelpers.setStaticField(Build.VERSION::class.java, SDK_INT, Build.VERSION_CODES.O) + + val testAlerting = Notify.defaultConfig.defaultAlerting + + Notify.with(this.context) + .content { + title = "New dessert menu" + text = "The Cheesecake Factory has a new dessert for you to try!" + } + .show() + + val shadowChannel = shadowNotificationManager.getNotificationChannel(testAlerting.channelKey) + Assert.assertNotNull(shadowChannel) + Assert.assertEquals(testAlerting.lockScreenVisibility, shadowChannel.lockscreenVisibility) + Assert.assertEquals(testAlerting.channelName, shadowChannel.name) + Assert.assertEquals(testAlerting.channelDescription, shadowChannel.description) + Assert.assertEquals(testAlerting.channelImportance + 2, shadowChannel.importance) + // Assert.assertEquals(testLightColor, shadowChannel.lightColor) + Assert.assertNull(shadowChannel.vibrationPattern) + Assert.assertEquals(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), shadowChannel.sound) } @Test - fun modifiedAlertingTest() { + fun modifiedAlertingTest_onAndroidN() { + ReflectionHelpers.setStaticField(Build.VERSION::class.java, SDK_INT, Build.VERSION_CODES.N_MR1) + val testVisibility = NotificationCompat.VISIBILITY_PUBLIC - val testTimeout = 5000L + val testChannelKey = "test_key" + val testChannelName = "Test Channel" + val testChannelDescription = "Test Channel Description" + val testChannelImportance = Notify.IMPORTANCE_HIGH + val testLightColor = Color.CYAN + val testVibrationPattern = listOf(0, 200, 0, 200) + val testSound = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE) val notification = Notify.with(this.context) - .alerting { + .alerting(testChannelKey) { lockScreenVisibility = testVisibility - timeout = testTimeout + channelName = testChannelName + channelDescription = testChannelDescription + channelImportance = testChannelImportance + lightColor = testLightColor + vibrationPattern = testVibrationPattern + sound = testSound } .content { title = "New dessert menu" @@ -40,7 +96,52 @@ class NotifyAlertingTest : NotifyTestBase() { .asBuilder() .build() + Assert.assertNull(notification.channelId) Assert.assertEquals(testVisibility, notification.visibility) - Assert.assertEquals(testTimeout, notification.timeoutAfter) + Assert.assertEquals(testChannelImportance, notification.priority) + // Color comparison nonsense again. + // Assert.assertEquals(testLightColor, notification.color) + Assert.assertEquals(testVibrationPattern, notification.vibrate.asList()) + Assert.assertEquals(testSound, notification.sound) + } + + @Test + fun modifiedAlertingTest_onAndroidO() { + ReflectionHelpers.setStaticField(Build.VERSION::class.java, SDK_INT, Build.VERSION_CODES.O) + + val testVisibility = NotificationCompat.VISIBILITY_PUBLIC + val testChannelKey = "test_key" + val testChannelName = "Test Channel" + val testChannelDescription = "Test Channel Description" + val testChannelImportance = Notify.IMPORTANCE_HIGH + val testLightColor = Color.CYAN + val testVibrationPattern = listOf(0, 200, 0, 200) + val testSound = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE) + + Notify.with(this.context) + .alerting(testChannelKey) { + lockScreenVisibility = testVisibility + channelName = testChannelName + channelDescription = testChannelDescription + channelImportance = testChannelImportance + lightColor = testLightColor + vibrationPattern = testVibrationPattern + sound = testSound + } + .content { + title = "New dessert menu" + text = "The Cheesecake Factory has a new dessert for you to try!" + } + .show() + + val shadowChannel = shadowNotificationManager.getNotificationChannel(testChannelKey) + Assert.assertNotNull(shadowChannel) + Assert.assertEquals(testVisibility, shadowChannel.lockscreenVisibility) + Assert.assertEquals(testChannelName, shadowChannel.name) + Assert.assertEquals(testChannelDescription, shadowChannel.description) + Assert.assertEquals(testChannelImportance + 2, shadowChannel.importance) + // Assert.assertEquals(testLightColor, shadowChannel.lightColor) + Assert.assertEquals(testVibrationPattern, shadowChannel.vibrationPattern.toList()) + Assert.assertEquals(testSound, shadowChannel.sound) } } diff --git a/library/src/test/java/io/karn/notify/NotifyChannelTest.kt b/library/src/test/java/io/karn/notify/NotifyChannelTest.kt deleted file mode 100644 index fd67bc6..0000000 --- a/library/src/test/java/io/karn/notify/NotifyChannelTest.kt +++ /dev/null @@ -1,55 +0,0 @@ -package io.karn.notify - -import android.app.NotificationManager -import android.os.Build -import android.support.v4.app.NotificationCompat -import org.junit.After -import org.junit.Assert -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.robolectric.shadow.api.Shadow -import org.robolectric.util.ReflectionHelpers - - -@RunWith(RobolectricTestRunner::class) -class NotifyChannelTest : NotifyTestBase() { - - private var currentSdkVersion = Build.VERSION.SDK_INT - - @After - fun runAfter() { - ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", currentSdkVersion) - } - - @Test - fun registerChannelTest_onAndroidO() { - ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", Build.VERSION_CODES.O) - - val notificationManager = Shadow.newInstanceOf(NotificationManager::class.java) - - val registeredChannel = NotifyChannel.registerChannel(notificationManager, - Notify.DEFAULT_CHANNEL_KEY, - Notify.DEFAULT_CHANNEL_NAME, - Notify.DEFAULT_CHANNEL_DESCRIPTION, - NotificationCompat.PRIORITY_DEFAULT) - - Assert.assertTrue(registeredChannel) - Assert.assertNotNull(notificationManager.getNotificationChannel(Notify.DEFAULT_CHANNEL_KEY)) - } - - @Test - fun registerChannelTest_onAndroidN() { - ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", Build.VERSION_CODES.N_MR1) - - val notificationManager = Shadow.newInstanceOf(NotificationManager::class.java) - - val registeredChannel = NotifyChannel.registerChannel(notificationManager, - Notify.DEFAULT_CHANNEL_KEY, - Notify.DEFAULT_CHANNEL_NAME, - Notify.DEFAULT_CHANNEL_DESCRIPTION, - NotificationCompat.PRIORITY_DEFAULT) - - Assert.assertFalse(registeredChannel) - } -} diff --git a/library/src/test/java/io/karn/notify/NotifyContentTest.kt b/library/src/test/java/io/karn/notify/NotifyContentTest.kt index 6e60c5c..b519b63 100644 --- a/library/src/test/java/io/karn/notify/NotifyContentTest.kt +++ b/library/src/test/java/io/karn/notify/NotifyContentTest.kt @@ -3,6 +3,7 @@ package io.karn.notify import android.app.Application import android.graphics.Bitmap import android.graphics.BitmapFactory +import android.graphics.drawable.Icon import android.net.Uri import android.os.Bundle import android.os.Parcelable @@ -12,7 +13,6 @@ import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment -import java.util.* @RunWith(RobolectricTestRunner::class) class NotifyContentTest { @@ -63,11 +63,13 @@ class NotifyContentTest { fun defaultNotification() { val testTitle = "New dessert menu" val testText = "The Cheesecake Factory has a new dessert for you to try!" + val testLargeIconResID = R.drawable.notification_tile_bg val notification = Notify.with(this.context) .content { title = testTitle text = testText + largeIcon = BitmapFactory.decodeResource(context.resources, testLargeIconResID) } .asBuilder() .build() @@ -75,6 +77,7 @@ class NotifyContentTest { Assert.assertNull(notification.extras.getCharSequence(NotificationCompat.EXTRA_TEMPLATE)) Assert.assertEquals(testTitle, notification.extras.getCharSequence(NotificationCompat.EXTRA_TITLE).toString()) Assert.assertEquals(testText, notification.extras.getCharSequence(NotificationCompat.EXTRA_TEXT).toString()) + Assert.assertEquals(context.resources.getDrawable(testLargeIconResID, context.theme), notification.getLargeIcon().loadDrawable(this.context)) } @Test @@ -118,7 +121,7 @@ class NotifyContentTest { .asBigText { title = testTitle text = testText - collapsedText = testExpandedText + expandedText = testExpandedText bigText = testBigText } .asBuilder() @@ -135,6 +138,7 @@ class NotifyContentTest { val testTitle = "Chocolate brownie sundae" val testText = "Get a look at this amazing dessert!" val testCollapsedText = "The delicious brownie sundae now available." + val testLargeIconResID = R.drawable.notification_tile_bg val testImage = BitmapFactory.decodeResource(context.resources, R.drawable.notification_tile_bg) Assert.assertNotNull(testImage) @@ -143,7 +147,8 @@ class NotifyContentTest { title = testTitle text = testText image = testImage - collapsedText = testCollapsedText + expandedText = testCollapsedText + largeIcon = BitmapFactory.decodeResource(context.resources, testLargeIconResID) } .asBuilder() .build() @@ -154,6 +159,11 @@ class NotifyContentTest { // This is an example of Notifications vague methods. The Builder#setSummaryText is // different from the Style#setSummaryText. 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) + Assert.assertNotNull(actualIcon) + val actualImage: Bitmap = notification.extras.getParcelable(NotificationCompat.EXTRA_PICTURE) Assert.assertNotNull(actualImage) diff --git a/library/src/test/java/io/karn/notify/NotifyHeaderTest.kt b/library/src/test/java/io/karn/notify/NotifyHeaderTest.kt index 99f43fc..6e551a2 100644 --- a/library/src/test/java/io/karn/notify/NotifyHeaderTest.kt +++ b/library/src/test/java/io/karn/notify/NotifyHeaderTest.kt @@ -13,7 +13,6 @@ class NotifyHeaderTest : NotifyTestBase() { @Test @Ignore fun defaultHeaderTest() { - val notification = Notify.with(this.context) .content { title = "New dessert menu" @@ -28,7 +27,6 @@ class NotifyHeaderTest : NotifyTestBase() { String.format("#%06X", context.resources.getColor(R.color.notification_header_color, context.theme)), String.format("#%06X", 0xFFFFFFFF and notification.color.toLong())) Assert.assertEquals(null, notification.extras.getCharSequence(NotificationCompat.EXTRA_SUB_TEXT)) - Assert.assertEquals(Notify.DEFAULT_CHANNEL_KEY, notification.channelId) Assert.assertTrue(notification.extras.getBoolean(NotificationCompat.EXTRA_SHOW_WHEN)) } @@ -37,7 +35,6 @@ class NotifyHeaderTest : NotifyTestBase() { val testIcon = R.drawable.ic_android_black val testColor = android.R.color.holo_purple val testHeaderText = "New Menu!" - val testChannel = "test_channel" val testShowTimestamp = false val notification = Notify.with(this.context) @@ -45,7 +42,6 @@ class NotifyHeaderTest : NotifyTestBase() { icon = testIcon color = testColor headerText = testHeaderText - channel = testChannel showTimestamp = testShowTimestamp } .content { @@ -60,7 +56,6 @@ class NotifyHeaderTest : NotifyTestBase() { String.format("#%06X", context.resources.getColor(testColor, context.theme)), String.format("#%06X", 0xFFFFFFFF and notification.color.toLong())) Assert.assertEquals(testHeaderText, notification.extras.getCharSequence(NotificationCompat.EXTRA_SUB_TEXT)) - Assert.assertEquals(testChannel, notification.channelId) Assert.assertEquals(testShowTimestamp, notification.extras.getBoolean(NotificationCompat.EXTRA_SHOW_WHEN)) } } diff --git a/library/src/test/java/io/karn/notify/NotifyMetaTest.kt b/library/src/test/java/io/karn/notify/NotifyMetaTest.kt index 66046d2..b398e16 100644 --- a/library/src/test/java/io/karn/notify/NotifyMetaTest.kt +++ b/library/src/test/java/io/karn/notify/NotifyMetaTest.kt @@ -1,6 +1,5 @@ package io.karn.notify -import android.app.Application import android.app.PendingIntent import android.content.Intent import android.provider.Settings @@ -9,7 +8,6 @@ import org.junit.Assert import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner -import org.robolectric.RuntimeEnvironment @RunWith(RobolectricTestRunner::class) class NotifyMetaTest : NotifyTestBase() { @@ -28,7 +26,6 @@ class NotifyMetaTest : NotifyTestBase() { Assert.assertNull(notification.deleteIntent) Assert.assertTrue((notification.flags and NotificationCompat.FLAG_AUTO_CANCEL) != 0) Assert.assertNull(notification.category) - Assert.assertEquals(NotificationCompat.PRIORITY_DEFAULT, notification.priority) } @Test @@ -38,7 +35,7 @@ class NotifyMetaTest : NotifyTestBase() { val testCancelOnClick = false val testCategory = NotificationCompat.CATEGORY_STATUS - val testPriority = NotificationCompat.PRIORITY_MAX + val testTimeout = 5000L val notification = Notify.with(this.context) .meta { @@ -46,7 +43,10 @@ class NotifyMetaTest : NotifyTestBase() { clearIntent = testClearIntent cancelOnClick = testCancelOnClick category = testCategory - priority = testPriority + timeout = testTimeout + people { + add("mailto:hello@test.com") + } } .content { title = "New dessert menu" @@ -59,6 +59,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(testPriority, notification.priority) + Assert.assertEquals(testTimeout, notification.timeoutAfter) + Assert.assertEquals(1, notification.extras.getStringArray(NotificationCompat.EXTRA_PEOPLE)?.size ?: 0) } } diff --git a/library/src/test/java/io/karn/notify/NotifyStackableTest.kt b/library/src/test/java/io/karn/notify/NotifyStackableTest.kt index 6747c48..3533901 100644 --- a/library/src/test/java/io/karn/notify/NotifyStackableTest.kt +++ b/library/src/test/java/io/karn/notify/NotifyStackableTest.kt @@ -4,8 +4,10 @@ import android.app.PendingIntent import android.content.Intent import android.provider.Settings import android.support.v4.app.NotificationCompat -import io.karn.notify.utils.Action -import io.karn.notify.utils.Errors +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 diff --git a/library/src/test/java/io/karn/notify/NotifyTest.kt b/library/src/test/java/io/karn/notify/NotifyTest.kt index aa27a43..e0d39ec 100644 --- a/library/src/test/java/io/karn/notify/NotifyTest.kt +++ b/library/src/test/java/io/karn/notify/NotifyTest.kt @@ -1,34 +1,71 @@ package io.karn.notify -import android.app.NotificationManager +import android.graphics.Color +import android.media.RingtoneManager +import android.support.v4.app.NotificationCompat +import io.karn.notify.internal.NotificationInterop import junit.framework.Assert import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner -import org.robolectric.RuntimeEnvironment -import org.robolectric.shadow.api.Shadow @RunWith(RobolectricTestRunner::class) -class NotifyTest { - - private val context = RuntimeEnvironment.application +class NotifyTest : NotifyTestBase() { @Test fun initializationTest() { + val testIcon = R.drawable.ic_android_black + val testColor = android.R.color.darker_gray + + val testVisibility = NotificationCompat.VISIBILITY_PUBLIC + val testChannelKey = "test_key_alt" + val testChannelName = "Test Channel" + val testChannelDescription = "Test Channel Description" + val testChannelImportance = Notify.IMPORTANCE_HIGH + val testLightColor = Color.CYAN + val testVibrationPattern = listOf(0, 200, 0, 200) + val testSound = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE) + Notify.defaultConfig { - it.header.icon = R.drawable.ic_android_black - it.header.color = android.R.color.darker_gray + header { + icon = testIcon + color = testColor + } + + alerting(testChannelKey) { + lockScreenVisibility = testVisibility + channelName = testChannelName + channelDescription = testChannelDescription + channelImportance = testChannelImportance + lightColor = testLightColor + vibrationPattern = testVibrationPattern + sound = testSound + } } + + val rawNotification = Notify.with(this.context) + .content { + title = "New dessert menu" + text = "The Cheesecake Factory has a new dessert for you to try!" + } + + Assert.assertEquals(context.resources.getDrawable(testIcon, context.theme), rawNotification.asBuilder().build().smallIcon.loadDrawable(context)) + + rawNotification.show() + + val shadowChannel = shadowNotificationManager.getNotificationChannel(testChannelKey) + org.junit.Assert.assertNotNull(shadowChannel) + org.junit.Assert.assertEquals(testVisibility, shadowChannel.lockscreenVisibility) + org.junit.Assert.assertEquals(testChannelName, shadowChannel.name) + org.junit.Assert.assertEquals(testChannelDescription, shadowChannel.description) + org.junit.Assert.assertEquals(testChannelImportance + 2, shadowChannel.importance) + // Assert.assertEquals(testLightColor, shadowChannel.lightColor) + org.junit.Assert.assertEquals(testVibrationPattern, shadowChannel.vibrationPattern.toList()) + org.junit.Assert.assertEquals(testSound, shadowChannel.sound) } @Test fun showNotification() { - val notificationManager = Shadow.newInstanceOf(NotificationManager::class.java) - - Notify.defaultConfig { - it.notificationManager = notificationManager - } - Notify.with(this.context) .content { title = "New dessert menu" @@ -36,18 +73,11 @@ class NotifyTest { } .show() - Assert.assertEquals(1, NotificationInterop.getActiveNotifications(notificationManager).size) + Assert.assertEquals(1, NotificationInterop.getActiveNotifications(shadowNotificationManager).size) } @Test fun cancelNotification() { - // TODO: Inject existing notifications so there is no code duplication. - val notificationManager = Shadow.newInstanceOf(NotificationManager::class.java) - - Notify.defaultConfig { - it.notificationManager = notificationManager - } - val notificationId = Notify.with(this.context) .content { title = "New dessert menu" @@ -55,11 +85,11 @@ class NotifyTest { } .show() - Assert.assertEquals(1, NotificationInterop.getActiveNotifications(notificationManager).size) + Assert.assertEquals(1, NotificationInterop.getActiveNotifications(shadowNotificationManager).size) Notify.with(this.context) .cancel(notificationId) - Assert.assertEquals(0, NotificationInterop.getActiveNotifications(notificationManager).size) + Assert.assertEquals(0, NotificationInterop.getActiveNotifications(shadowNotificationManager).size) } } diff --git a/library/src/test/java/io/karn/notify/NotifyTestBase.kt b/library/src/test/java/io/karn/notify/NotifyTestBase.kt index 58a63d7..0b5effa 100644 --- a/library/src/test/java/io/karn/notify/NotifyTestBase.kt +++ b/library/src/test/java/io/karn/notify/NotifyTestBase.kt @@ -2,19 +2,33 @@ package io.karn.notify import android.app.Application import android.app.NotificationManager +import android.os.Build +import io.karn.notify.entities.Payload +import io.karn.notify.internal.NotificationChannelInterop import org.junit.Before import org.robolectric.RuntimeEnvironment import org.robolectric.shadow.api.Shadow open class NotifyTestBase { + + companion object { + @JvmStatic + protected val SDK_INT = "SDK_INT" + @JvmStatic + protected var currentSdkVersion = Build.VERSION.SDK_INT + } + protected val context: Application = RuntimeEnvironment.application + protected var shadowNotificationManager: NotificationManager = Shadow.newInstanceOf(NotificationManager::class.java) @Before - fun resetNotificationManager() { - val notificationManager = Shadow.newInstanceOf(NotificationManager::class.java) - + fun setNotificationManager() { + shadowNotificationManager = Shadow.newInstanceOf(NotificationManager::class.java) Notify.defaultConfig { - it.notificationManager = notificationManager + defaultHeader = Payload.Header() + defaultAlerting = Payload.Alerts() + notificationManager = shadowNotificationManager } + NotificationChannelInterop.with(Notify.defaultConfig.defaultAlerting) } } diff --git a/sample/src/main/java/presentation/MainActivity.kt b/sample/src/main/java/presentation/MainActivity.kt index c39bfcd..5299772 100644 --- a/sample/src/main/java/presentation/MainActivity.kt +++ b/sample/src/main/java/presentation/MainActivity.kt @@ -1,6 +1,7 @@ package presentation import android.graphics.BitmapFactory +import android.graphics.Color import android.os.Bundle import android.support.v4.app.NotificationCompat import android.support.v7.app.AppCompatActivity @@ -16,7 +17,12 @@ class MainActivity : AppCompatActivity() { setContentView(R.layout.activity_main) Notify.defaultConfig { - it.header.color = R.color.colorPrimaryDark + header { + color = R.color.colorPrimaryDark + } + alerting(Notify.CHANNEL_DEFAULT_KEY) { + lightColor = Color.RED + } } } @@ -56,7 +62,7 @@ class MainActivity : AppCompatActivity() { .asBigText { title = "Chocolate brownie sundae" text = "Try our newest dessert option!" - collapsedText = "Try our newest dessert option!" + expandedText = "Mouthwatering deliciousness." bigText = "Our own Fabulous Godiva Chocolate Brownie, Vanilla Ice Cream, Hot Fudge, Whipped Cream and Toasted Almonds.\n" + "\n" + "Come try this delicious new dessert and get two for the price of one!" @@ -70,7 +76,7 @@ class MainActivity : AppCompatActivity() { .asBigPicture { title = "Chocolate brownie sundae" text = "Get a look at this amazing dessert!" - collapsedText = "The delicious brownie sundae now available." + expandedText = "The delicious brownie sundae now available." image = BitmapFactory.decodeResource(this@MainActivity.resources, R.drawable.chocolate_brownie_sundae) } .show()