Skip to content

Commit

Permalink
[Release] Feature Notification- sub feature "Delete Notifications" "S…
Browse files Browse the repository at this point in the history
…ystem Notification" #39

* release note - feature notification, sub feature complete "Delete Notifications" "System Notifications"

* release note - version = 1.4.0
  • Loading branch information
syedahmedjamil committed Jan 23, 2024
2 parents 5f7afd3 + f89cfb7 commit 2636f6d
Show file tree
Hide file tree
Showing 47 changed files with 419 additions and 120 deletions.
8 changes: 5 additions & 3 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ android {
minSdk = 24
targetSdk = 33
versionCode = project.property("versionCode").toString().toInt()
versionName = "1.3.1"
versionName = "1.4.0"
testApplicationId = "com.github.syedahmedjamil.pushernotif.test"
// testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
// testInstrumentationRunner = "io.cucumber.android.runner.CucumberAndroidJUnitRunner"
Expand Down Expand Up @@ -59,8 +59,8 @@ android {
"proguard-rules.pro"
)
resValue("string", "app_name", "Pusher Notif (${suffix})")
isMinifyEnabled = true
isShrinkResources = true
isMinifyEnabled = false
isShrinkResources = false
isDebuggable = false
versionNameSuffix = "-${suffix}"
signingConfig = signingConfigs.getByName("release")
Expand Down Expand Up @@ -133,6 +133,8 @@ android {
implementation("com.google.firebase:firebase-iid:21.1.0") // for pusher
implementation(platform("com.google.firebase:firebase-bom:32.7.0"))
implementation("com.google.firebase:firebase-messaging")
implementation("com.google.firebase:firebase-crashlytics")
implementation("com.google.firebase:firebase-analytics")
implementation("com.pusher:push-notifications-android:1.9.2")
implementation("androidx.navigation:navigation-ui-ktx:2.7.6")
implementation("androidx.navigation:navigation-fragment-ktx:2.7.6")
Expand Down
1 change: 1 addition & 0 deletions app/cert.base64
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ewogICJwcm9qZWN0X2luZm8iOiB7CiAgICAicHJvamVjdF9udW1iZXIiOiAiNjc4NzM4MTUyNjU4IiwKICAgICJwcm9qZWN0X2lkIjogIm1haW4tN2FjZjgiLAogICAgInN0b3JhZ2VfYnVja2V0IjogIm1haW4tN2FjZjguYXBwc3BvdC5jb20iCiAgfSwKICAiY2xpZW50IjogWwogICAgewogICAgICAiY2xpZW50X2luZm8iOiB7CiAgICAgICAgIm1vYmlsZXNka19hcHBfaWQiOiAiMTo2Nzg3MzgxNTI2NTg6YW5kcm9pZDpkYTJjNDIzODllZmUzZmE3NDY3MTBiIiwKICAgICAgICAiYW5kcm9pZF9jbGllbnRfaW5mbyI6IHsKICAgICAgICAgICJwYWNrYWdlX25hbWUiOiAiY29tLmV4YW1wbGUuaGlsdHRlc3QiCiAgICAgICAgfQogICAgICB9LAogICAgICAib2F1dGhfY2xpZW50IjogW10sCiAgICAgICJhcGlfa2V5IjogWwogICAgICAgIHsKICAgICAgICAgICJjdXJyZW50X2tleSI6ICJBSXphU3lCTzl5MlJnZkJWekl4ZG1FSDV4a2JwSm4xS2w2Mk9ZY1EiCiAgICAgICAgfQogICAgICBdLAogICAgICAic2VydmljZXMiOiB7CiAgICAgICAgImFwcGludml0ZV9zZXJ2aWNlIjogewogICAgICAgICAgIm90aGVyX3BsYXRmb3JtX29hdXRoX2NsaWVudCI6IFtdCiAgICAgICAgfQogICAgICB9CiAgICB9LAogICAgewogICAgICAiY2xpZW50X2luZm8iOiB7CiAgICAgICAgIm1vYmlsZXNka19hcHBfaWQiOiAiMTo2Nzg3MzgxNTI2NTg6YW5kcm9pZDo1ZTY1OTExZDVmNGQ0YTdjNDY3MTBiIiwKICAgICAgICAiYW5kcm9pZF9jbGllbnRfaW5mbyI6IHsKICAgICAgICAgICJwYWNrYWdlX25hbWUiOiAiY29tLmV4YW1wbGUucHVzaGVyc2RrdGVzdGluZyIKICAgICAgICB9CiAgICAgIH0sCiAgICAgICJvYXV0aF9jbGllbnQiOiBbXSwKICAgICAgImFwaV9rZXkiOiBbCiAgICAgICAgewogICAgICAgICAgImN1cnJlbnRfa2V5IjogIkFJemFTeUJPOXkyUmdmQlZ6SXhkbUVINXhrYnBKbjFLbDYyT1ljUSIKICAgICAgICB9CiAgICAgIF0sCiAgICAgICJzZXJ2aWNlcyI6IHsKICAgICAgICAiYXBwaW52aXRlX3NlcnZpY2UiOiB7CiAgICAgICAgICAib3RoZXJfcGxhdGZvcm1fb2F1dGhfY2xpZW50IjogW10KICAgICAgICB9CiAgICAgIH0KICAgIH0sCiAgICB7CiAgICAgICJjbGllbnRfaW5mbyI6IHsKICAgICAgICAibW9iaWxlc2RrX2FwcF9pZCI6ICIxOjY3ODczODE1MjY1ODphbmRyb2lkOjViMGI4ZGQzMDA1NzYxZmE0NjcxMGIiLAogICAgICAgICJhbmRyb2lkX2NsaWVudF9pbmZvIjogewogICAgICAgICAgInBhY2thZ2VfbmFtZSI6ICJjb20uZ2l0aHViLnN5ZWRhaG1lZGphbWlsLnB1c2hlcm5vdGlmIgogICAgICAgIH0KICAgICAgfSwKICAgICAgIm9hdXRoX2NsaWVudCI6IFtdLAogICAgICAiYXBpX2tleSI6IFsKICAgICAgICB7CiAgICAgICAgICAiY3VycmVudF9rZXkiOiAiQUl6YVN5Qk85eTJSZ2ZCVnpJeGRtRUg1eGticEpuMUtsNjJPWWNRIgogICAgICAgIH0KICAgICAgXSwKICAgICAgInNlcnZpY2VzIjogewogICAgICAgICJhcHBpbnZpdGVfc2VydmljZSI6IHsKICAgICAgICAgICJvdGhlcl9wbGF0Zm9ybV9vYXV0aF9jbGllbnQiOiBbXQogICAgICAgIH0KICAgICAgfQogICAgfSwKICAgIHsKICAgICAgImNsaWVudF9pbmZvIjogewogICAgICAgICJtb2JpbGVzZGtfYXBwX2lkIjogIjE6Njc4NzM4MTUyNjU4OmFuZHJvaWQ6YTUxMDI4M2MwYWJkMDczYzQ2NzEwYiIsCiAgICAgICAgImFuZHJvaWRfY2xpZW50X2luZm8iOiB7CiAgICAgICAgICAicGFja2FnZV9uYW1lIjogImNvbS5naXRodWIuc3llZGFobWVkamFtaWwucHVzaGVybm90aWYuZGVidWciCiAgICAgICAgfQogICAgICB9LAogICAgICAib2F1dGhfY2xpZW50IjogW10sCiAgICAgICJhcGlfa2V5IjogWwogICAgICAgIHsKICAgICAgICAgICJjdXJyZW50X2tleSI6ICJBSXphU3lCTzl5MlJnZkJWekl4ZG1FSDV4a2JwSm4xS2w2Mk9ZY1EiCiAgICAgICAgfQogICAgICBdLAogICAgICAic2VydmljZXMiOiB7CiAgICAgICAgImFwcGludml0ZV9zZXJ2aWNlIjogewogICAgICAgICAgIm90aGVyX3BsYXRmb3JtX29hdXRoX2NsaWVudCI6IFtdCiAgICAgICAgfQogICAgICB9CiAgICB9LAogICAgewogICAgICAiY2xpZW50X2luZm8iOiB7CiAgICAgICAgIm1vYmlsZXNka19hcHBfaWQiOiAiMTo2Nzg3MzgxNTI2NTg6YW5kcm9pZDo2M2UxZTUyOTVlMWY2NWZhNDY3MTBiIiwKICAgICAgICAiYW5kcm9pZF9jbGllbnRfaW5mbyI6IHsKICAgICAgICAgICJwYWNrYWdlX25hbWUiOiAiY29tLmdpdGh1Yi5zeWVkYWhtZWRqYW1pbC5wdXNoZXJub3RpZi5yZWxlYXNlIgogICAgICAgIH0KICAgICAgfSwKICAgICAgIm9hdXRoX2NsaWVudCI6IFtdLAogICAgICAiYXBpX2tleSI6IFsKICAgICAgICB7CiAgICAgICAgICAiY3VycmVudF9rZXkiOiAiQUl6YVN5Qk85eTJSZ2ZCVnpJeGRtRUg1eGticEpuMUtsNjJPWWNRIgogICAgICAgIH0KICAgICAgXSwKICAgICAgInNlcnZpY2VzIjogewogICAgICAgICJhcHBpbnZpdGVfc2VydmljZSI6IHsKICAgICAgICAgICJvdGhlcl9wbGF0Zm9ybV9vYXV0aF9jbGllbnQiOiBbXQogICAgICAgIH0KICAgICAgfQogICAgfQogIF0sCiAgImNvbmZpZ3VyYXRpb25fdmVyc2lvbiI6ICIxIgp9
15 changes: 15 additions & 0 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Gson uses generic type information stored in a class file when working with fields.
# Proguard removes such information by default, so configure it to keep all of it.
-keepattributes Signature
-keepattributes EnclosingMethod
-keepattributes InnerClasses
-keepattributes Annotation

# For using GSON @Expose annotation
-keepattributes *Annotation*

# Gson specific classes
-keep class sun.misc.Unsafe { *; }

# Application classes that will be serialized/deserialized over Gson
-keep class com.google.gson.examples.android.model.** { *; }

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import kotlinx.coroutines.runBlocking
import javax.inject.Inject

@HiltAndroidTest
class FeatureInstanceSteps(
class Steps(
val scenarioHolder: ActivityScenarioHolder
) {

Expand Down Expand Up @@ -105,4 +105,5 @@ class FeatureInstanceSteps(
fun internetConnectionIsTurned(arg0: String) {
dsl.instance.setInternetConnection(arg0)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ package com.github.syedahmedjamil.pushernotif.test.integration
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import com.github.syedahmedjamil.pushernotif.AppContainer
import com.github.syedahmedjamil.pushernotif.domain.ImageLoader
import com.github.syedahmedjamil.pushernotif.ImageLoader
import com.github.syedahmedjamil.pushernotif.usecases.AddNotificationUseCase
import com.github.syedahmedjamil.pushernotif.usecases.DeleteNotificationsUseCase
import com.github.syedahmedjamil.pushernotif.usecases.GetNotificationsUseCase
import org.junit.Assert
import org.junit.Test
Expand All @@ -31,6 +32,12 @@ class AppContainerTest {
Assert.assertNotNull(dep)
}

@Test
fun test_appContainer_has_a_deleteNotificationsUseCase() {
val dep: DeleteNotificationsUseCase = appContainer.deleteNotificationsUseCase
Assert.assertNotNull(dep)
}

@Test
fun test_appContainer_has_a_imageLoader() {
val dep: ImageLoader = appContainer.imageLoader
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.github.syedahmedjamil.pushernotif.test.integration

import android.graphics.Bitmap
import com.github.syedahmedjamil.pushernotif.ImageLoader

class FakeImageLoader: ImageLoader {
override fun getBase64(uri: String): String {
return "base64Image"
}

override fun getBitmap(): Bitmap {
return Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@ import android.content.ContextWrapper
import androidx.test.core.app.ApplicationProvider
import com.github.syedahmedjamil.pushernotif.AppContainer
import com.github.syedahmedjamil.pushernotif.BaseApplication
import com.github.syedahmedjamil.pushernotif.ImageLoader
import com.github.syedahmedjamil.pushernotif.core.Result
import com.github.syedahmedjamil.pushernotif.domain.ImageLoader
import com.github.syedahmedjamil.pushernotif.domain.NotificationEntity
import com.github.syedahmedjamil.pushernotif.framework.MyPusherMessagingService
import com.github.syedahmedjamil.pushernotif.shared_test.fakes.FakeImageLoader
import com.github.syedahmedjamil.pushernotif.shared_test.fakes.usecase.FakeAddNotificationUseCase
import com.github.syedahmedjamil.pushernotif.usecases.AddNotificationUseCase
import com.google.firebase.messaging.RemoteMessage
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
Expand Down Expand Up @@ -73,7 +72,7 @@ class NotificationLocalDataSourceTest {
// when
val actual = notificationDataSource.getNotifications("test").first()
// then
Assert.assertEquals(expected, actual)
assertEquals(expected, actual)
}

@Test
Expand Down Expand Up @@ -104,4 +103,35 @@ class NotificationLocalDataSourceTest {
assertEquals(expected2, actual2)
}

@Test
fun should_delete_notifications_when_exists() = testScope.runTest {
// given
createTestDataStoreFile("test_notification.preferences_pb")
val interest1 = "test1"
val interest2 = "test2"
val expected = emptyList<NotificationEntity>()
// when
notificationDataSource.deleteNotifications()
val actual1 = notificationDataSource.getNotifications(interest1).first()
val actual2 = notificationDataSource.getNotifications(interest2).first()
// then
assertEquals(expected, actual1)
assertEquals(expected, actual2)
}

@Test
fun should_delete_notifications_when_not_exists() = testScope.runTest {
// given
val interest1 = "test1"
val interest2 = "test2"
val expected = emptyList<NotificationEntity>()
// when
notificationDataSource.deleteNotifications()
val actual1 = notificationDataSource.getNotifications(interest1).first()
val actual2 = notificationDataSource.getNotifications(interest2).first()
// then
assertEquals(expected, actual1)
assertEquals(expected, actual2)
}

}
3 changes: 3 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<application
android:name=".MyApplication"
android:allowBackup="true"
Expand All @@ -15,6 +17,7 @@
tools:targetApi="31">
<activity
android:name=".ui.MainActivity"
android:windowSoftInputMode="adjustPan"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ import androidx.annotation.VisibleForTesting
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.preferencesDataStoreFile
import com.github.syedahmedjamil.pushernotif.data.NotificationRepositoryImpl
import com.github.syedahmedjamil.pushernotif.domain.ImageLoader
import com.github.syedahmedjamil.pushernotif.framework.NotificationLocalDataSource
import com.github.syedahmedjamil.pushernotif.framework.PicassoImageLoader
import com.github.syedahmedjamil.pushernotif.usecases.AddNotificationUseCase
import com.github.syedahmedjamil.pushernotif.usecases.AddNotificationUseCaseImpl
import com.github.syedahmedjamil.pushernotif.usecases.DeleteNotificationsUseCase
import com.github.syedahmedjamil.pushernotif.usecases.DeleteNotificationsUseCaseImpl
import com.github.syedahmedjamil.pushernotif.usecases.GetNotificationsUseCase
import com.github.syedahmedjamil.pushernotif.usecases.GetNotificationsUseCaseImpl
import kotlinx.coroutines.runBlocking
Expand Down Expand Up @@ -41,6 +42,9 @@ class AppContainer(context: Context) {
val getNotificationsUseCase: GetNotificationsUseCase =
GetNotificationsUseCaseImpl(notificationRepository)

val deleteNotificationsUseCase: DeleteNotificationsUseCase =
DeleteNotificationsUseCaseImpl(notificationRepository)

// These are var because they are used in tests to replace with fakes
var addNotificationUseCase: AddNotificationUseCase =
AddNotificationUseCaseImpl(notificationRepository)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.github.syedahmedjamil.pushernotif

import android.graphics.Bitmap

interface ImageLoader {
fun getBase64(uri: String): String
fun getBitmap(): Bitmap
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,29 @@
package com.github.syedahmedjamil.pushernotif.framework

import androidx.annotation.VisibleForTesting
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.net.Uri
import android.os.Build
import androidx.core.app.NotificationCompat
import com.github.syedahmedjamil.pushernotif.AppContainer
import com.github.syedahmedjamil.pushernotif.BaseApplication
import com.github.syedahmedjamil.pushernotif.domain.ImageLoader
import com.github.syedahmedjamil.pushernotif.ImageLoader
import com.github.syedahmedjamil.pushernotif.R
import com.github.syedahmedjamil.pushernotif.domain.NotificationEntity
import com.github.syedahmedjamil.pushernotif.usecases.AddNotificationUseCase
import com.google.firebase.messaging.RemoteMessage
import com.pusher.pushnotifications.fcm.MessagingService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.Arrays
import java.util.stream.Collectors


class MyPusherMessagingService : MessagingService() {

Expand All @@ -24,15 +37,7 @@ class MyPusherMessagingService : MessagingService() {
imageLoader = appContainer.imageLoader
}

@VisibleForTesting
companion object {
var isOnMessageReceivedCalled = false
lateinit var data: MutableMap<String, String>
}

override fun onMessageReceived(remoteMessage: RemoteMessage) {
data = remoteMessage.data
isOnMessageReceivedCalled = true

val title = remoteMessage.data["title"]!!
val body = remoteMessage.data["body"]!!
Expand All @@ -42,7 +47,7 @@ class MyPusherMessagingService : MessagingService() {
val image = remoteMessage.data["image"]!!
val interest = remoteMessage.data["interest"]!!

val base64Image = imageLoader.load(image)
val base64Image = imageLoader.getBase64(image)

val notification = NotificationEntity(
title = title,
Expand All @@ -54,10 +59,69 @@ class MyPusherMessagingService : MessagingService() {
interest = interest
)

storeNotification(notification)
showNotification(notification)
}

private fun showNotification(notification: NotificationEntity) {

val id = System.currentTimeMillis().toInt()

val webpage = Uri.parse(notification.link)
val intent = Intent(Intent.ACTION_VIEW, webpage)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
val chooser = Intent.createChooser(intent, "Select Browser")

val pendingIntent = PendingIntent.getActivity(
this,
id,
chooser,
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
)

var builder = NotificationCompat.Builder(this, "CHANNEL_ID")
.setSmallIcon(R.drawable.ic_android_black_24dp)
.setLargeIcon(imageLoader.getBitmap())
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentTitle(notification.title)
.setSubText(notification.subText)
.setStyle(NotificationCompat.BigTextStyle().bigText(notification.body))
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.setColor(Color.BLACK)
.setGroup("pushier")
.setLights(Color.MAGENTA, 1000, 300)
.setDefaults(Notification.DEFAULT_VIBRATE)

val notificationManager: NotificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val name = "cn"
val descriptionText = "cd"
val importance = NotificationManager.IMPORTANCE_DEFAULT
val channel = NotificationChannel("CHANNEL_ID", name, importance).apply {
description = descriptionText
}
// Register the channel with the system.
notificationManager.createNotificationChannel(channel)
}

val activeNotifications = notificationManager.activeNotifications
if (activeNotifications.size == 20) {
val s = Arrays.stream(activeNotifications).sorted { statusBarNotification, t1 ->
statusBarNotification.postTime.toString().compareTo(t1.postTime.toString())
}.collect(Collectors.toList())
notificationManager.cancel(s[0].id)
}

notificationManager.notify(id, builder.build())

}

private fun storeNotification(notification: NotificationEntity) {
CoroutineScope(Dispatchers.Main).launch {
addNotificationUseCase(notification)
}
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,8 @@ class NotificationLocalDataSource @Inject constructor(private val dataStore: Dat
preferences[stringPreferencesKey(UUID.randomUUID().toString())] = notificationJson
}
}

override suspend fun deleteNotifications() {
dataStore.edit { it.clear() }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,22 @@ package com.github.syedahmedjamil.pushernotif.framework

import android.graphics.Bitmap
import android.util.Base64
import com.github.syedahmedjamil.pushernotif.domain.ImageLoader
import com.github.syedahmedjamil.pushernotif.ImageLoader
import com.squareup.picasso.Picasso
import java.io.ByteArrayOutputStream

class PicassoImageLoader : ImageLoader {
override fun load(uri: String): String {
val icon = Picasso.get().load(uri).get()

private lateinit var icon: Bitmap
override fun getBase64(uri: String): String {
icon = Picasso.get().load(uri).get()
val baos = ByteArrayOutputStream()
icon.compress(Bitmap.CompressFormat.PNG, 100, baos)
val base64Image = Base64.encodeToString(baos.toByteArray(), Base64.DEFAULT)
return base64Image
}

override fun getBitmap(): Bitmap {
return icon
}
}
Loading

0 comments on commit 2636f6d

Please sign in to comment.