Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Release] Feature Notification- sub feature "Delete Notifications" "System Notification" #39

Merged
merged 2 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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