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()
+```
diff --git a/docs/assets/types/progress.png b/docs/assets/types/progress.png
new file mode 100644
index 0000000..67ee1d7
Binary files /dev/null and b/docs/assets/types/progress.png differ
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/gradle/configuration.gradle b/gradle/configuration.gradle
index 83f549d..c79f238 100644
--- a/gradle/configuration.gradle
+++ b/gradle/configuration.gradle
@@ -7,11 +7,11 @@
def versions = [
- libCode: 12,
- libName: '1.3.0',
+ libCode: 13,
+ libName: '1.4.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 f746bb3..6ef5d16 100644
--- a/library/build.gradle
+++ b/library/build.gradle
@@ -57,6 +57,11 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
+ // 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/Notify.kt b/library/src/main/java/io/karn/notify/Notify.kt
index 8c76d8e..c314b3b 100644
--- a/library/src/main/java/io/karn/notify/Notify.kt
+++ b/library/src/main/java/io/karn/notify/Notify.kt
@@ -108,8 +108,22 @@ 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)
+ return NotificationInterop.cancelNotification(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)
}
}
@@ -145,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 4e16a3d..abfd856 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,7 +45,9 @@ 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
+ private var progress: Payload.Progress = Payload.Progress()
/**
* Scoped function for modifying the Metadata of a notification, such as click intents,
@@ -63,8 +68,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
}
@@ -78,12 +82,16 @@ 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.
*/
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 +99,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 +107,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 +115,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 +123,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 +132,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 +180,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,12 +194,12 @@ 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, progress))
}
/**
- * 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.
*
@@ -170,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.
*
@@ -181,8 +237,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/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 bace5ae..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,13 +24,16 @@
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
+import androidx.core.graphics.drawable.IconCompat
import io.karn.notify.Notify
import io.karn.notify.R
import io.karn.notify.internal.utils.Action
@@ -64,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.
*/
@@ -140,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
)
/**
@@ -166,6 +178,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.
*/
@@ -272,6 +308,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/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 9d52c15..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.
@@ -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)
@@ -207,6 +212,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..eb196b9 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,8 @@ 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?
+ internal val actions: ArrayList?,
+ internal val progress: Payload.Progress
)
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/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? {
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/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/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/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/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)
}
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..73b773b 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() {
@@ -51,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!"
@@ -67,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.",
@@ -81,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!"
@@ -95,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!"
@@ -107,8 +108,7 @@ class MainActivity : AppCompatActivity() {
}
fun notifyMessage(view: View) {
- Notify
- .with(this)
+ Notify.with(this)
.asMessage {
userDisplayName = "Karn"
conversationTitle = "Sundae chat"
@@ -126,4 +126,57 @@ 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()
+ }
+
+ 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_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..5c3355e 100644
--- a/sample/src/main/res/layout/activity_main.xml
+++ b/sample/src/main/res/layout/activity_main.xml
@@ -72,4 +72,25 @@
android:layout_height="wrap_content"
android:onClick="notifyMessage"
android:text="Notify messages!" />
+
+
+
+
+
+