diff --git a/app/src/main/java/com/isolaatti/MyApplication.kt b/app/src/main/java/com/isolaatti/MyApplication.kt index 1f65939..6fe80a8 100644 --- a/app/src/main/java/com/isolaatti/MyApplication.kt +++ b/app/src/main/java/com/isolaatti/MyApplication.kt @@ -1,21 +1,24 @@ package com.isolaatti import android.app.Application +import android.app.NotificationChannel +import android.app.NotificationManager import android.content.Context import android.net.ConnectivityManager +import android.os.Build import android.util.Log +import androidx.annotation.RequiresApi import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.preferencesDataStore -import com.google.firebase.Firebase import com.google.firebase.messaging.FirebaseMessaging import com.isolaatti.connectivity.ConnectivityCallbackImpl -import com.isolaatti.push_notifications.FcmService import com.isolaatti.push_notifications.PushNotificationsApi import dagger.hilt.android.HiltAndroidApp import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import okhttp3.RequestBody.Companion.toRequestBody import retrofit2.awaitResponse import javax.inject.Inject @@ -27,6 +30,8 @@ class MyApplication : Application() { companion object { lateinit var myApp: MyApplication const val LOG_TAG = "MyApplication" + + const val LIKES_NOTIFICATION_CHANNEL_ID = "like notification" } @Inject @@ -42,18 +47,27 @@ class MyApplication : Application() { connectivityCallbackImpl = ConnectivityCallbackImpl() getSystemService(ConnectivityManager::class.java).registerDefaultNetworkCallback(connectivityCallbackImpl) - FirebaseMessaging.getInstance().token.addOnCompleteListener { - if(!it.isSuccessful) { - Log.w(LOG_TAG, "Failed fetching fcm token") - return@addOnCompleteListener - } - + FirebaseMessaging.getInstance().token.addOnSuccessListener { CoroutineScope(Dispatchers.IO).launch { - val response = pushNotificationsApi.registerDevice(it.result).awaitResponse() - Log.d(FcmService.LOG_TAG, "Device registered. FCM token: $it.result") - Log.d(FcmService.LOG_TAG, "Response: isSuccessful: ${response.isSuccessful}") + val response = pushNotificationsApi.registerDevice(it.toRequestBody()).awaitResponse() } } + + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager + createLikesNotificationChannel(notificationManager) + } + } + + @RequiresApi(Build.VERSION_CODES.O) + private fun createLikesNotificationChannel(notificationManager: NotificationManager) { + val name = getString(R.string.likes_notification_channel) + val description = getString(R.string.likes_notification_channel_description) + val importance = NotificationManager.IMPORTANCE_DEFAULT + val channel = NotificationChannel(LIKES_NOTIFICATION_CHANNEL_ID, name, importance).also { + it.description = description + } + notificationManager.createNotificationChannel(channel) } override fun onTerminate() { diff --git a/app/src/main/java/com/isolaatti/common/CoilImageLoader.kt b/app/src/main/java/com/isolaatti/common/CoilImageLoader.kt index 2f49474..baace6e 100644 --- a/app/src/main/java/com/isolaatti/common/CoilImageLoader.kt +++ b/app/src/main/java/com/isolaatti/common/CoilImageLoader.kt @@ -19,4 +19,17 @@ object CoilImageLoader { add(SvgDecoder.Factory()) }.build() } + + fun getImageLoader(context: Context): ImageLoader { + return ImageLoader + .Builder(context) + .memoryCache { + MemoryCache.Builder(context) + .maxSizePercent(0.25) + .build() + } + .components { + add(SvgDecoder.Factory()) + }.build() + } } \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/push_notifications/FcmService.kt b/app/src/main/java/com/isolaatti/push_notifications/FcmService.kt index c3d3d81..eeb72cb 100644 --- a/app/src/main/java/com/isolaatti/push_notifications/FcmService.kt +++ b/app/src/main/java/com/isolaatti/push_notifications/FcmService.kt @@ -1,22 +1,52 @@ package com.isolaatti.push_notifications import android.Manifest +import android.content.pm.PackageManager import android.util.Log -import androidx.core.content.ContextCompat +import androidx.core.app.ActivityCompat +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.graphics.drawable.toBitmap +import coil.Coil +import coil.request.ImageRequest import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage -import dagger.hilt.EntryPoint +import com.isolaatti.MyApplication +import com.isolaatti.R +import com.isolaatti.common.CoilImageLoader +import com.isolaatti.notifications.domain.LikeNotification +import com.isolaatti.utils.UrlGen import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.ResponseBody.Companion.toResponseBody import retrofit2.awaitResponse +import retrofit2.http.Url import javax.inject.Inject @AndroidEntryPoint class FcmService : FirebaseMessagingService() { companion object { const val LOG_TAG = "FcmService" + + object NotificationsConstants { + + const val NOTIFICATION_ID = "id" + const val TIMESTAMP = "timeStamp" + const val USER_ID = "userId" + const val READ = "read" + const val RELATED_NOTIFICATIONS = "relatedNotifications" + + object LikeNotificationConstants { + const val LIKE_ID = "likeId" + const val POST_ID = "postId" + const val AUTHOR_ID = "authorId" + const val AUTHOR_NAME = "authorName" + } + } + } @Inject @@ -25,19 +55,70 @@ class FcmService : FirebaseMessagingService() { override fun onNewToken(token: String) { CoroutineScope(Dispatchers.IO).launch { - val response = pushNotificationsApi.registerDevice(token).awaitResponse() - Log.d(LOG_TAG, "Device registered. FCM token: $token") - Log.d(LOG_TAG, "Response: isSuccessful: ${response.isSuccessful}") + val response = pushNotificationsApi.registerDevice(token.toRequestBody()).awaitResponse() } } override fun onMessageReceived(message: RemoteMessage) { super.onMessageReceived(message) + Log.d(LOG_TAG, "onMessageReceived") + val type = message.data["type"] - Log.d(LOG_TAG, "Message received") - message.data.forEach { t, u -> - Log.d(LOG_TAG, "$t $u") + when(type) { + LikeNotification.TYPE -> showLikeNotification(message.data) + + else -> { + Log.i(LOG_TAG, "Not showing notification of unknown type: ${message.data}") + } } } + + private fun showLikeNotification(data: Map) { + val notificationId = data[NotificationsConstants.NOTIFICATION_ID]?.toIntOrNull() + val relatedNotifications = data[NotificationsConstants.RELATED_NOTIFICATIONS]?.trimStart('[')?.trimEnd(']')?.split(",") + val likeId = data[NotificationsConstants.LikeNotificationConstants.LIKE_ID] + val postId = data[NotificationsConstants.LikeNotificationConstants.POST_ID] + val authorId = data[NotificationsConstants.LikeNotificationConstants.AUTHOR_ID]?.toIntOrNull() + val authorName = data[NotificationsConstants.LikeNotificationConstants.AUTHOR_NAME] + + + val imageUrl = authorId?.let { UrlGen.userProfileImage(it, true) } + + Log.d(LOG_TAG, data.toString()) + + val imageRequest = ImageRequest + .Builder(this) + .data(imageUrl) + .fallback(R.drawable.baseline_person_24) + .target { drawable -> + val notificationBuilder = NotificationCompat.Builder(this, MyApplication.LIKES_NOTIFICATION_CHANNEL_ID) + .setSmallIcon(R.drawable.ic_notification) + .setContentTitle(getString(R.string.like_notification_title, authorName ?: "")) + .setContentText(getString(R.string.like_notification_text)) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setLargeIcon(drawable.toBitmap()) + + if (ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { + return@target + } + + Log.v(LOG_TAG, "Notification id: $notificationId") + // notificationId should never be null + if(notificationId == null) { + return@target + } + NotificationManagerCompat.from(this).run { + relatedNotifications?.forEach { + it.toIntOrNull()?.let { relatedNotificationId -> cancel(relatedNotificationId) } + } + + + notify(notificationId, notificationBuilder.build()) + } + }.build() + + + CoilImageLoader.getImageLoader(this).enqueue(imageRequest) + } } \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/push_notifications/PushNotificationsApi.kt b/app/src/main/java/com/isolaatti/push_notifications/PushNotificationsApi.kt index bfd97a7..5f463d8 100644 --- a/app/src/main/java/com/isolaatti/push_notifications/PushNotificationsApi.kt +++ b/app/src/main/java/com/isolaatti/push_notifications/PushNotificationsApi.kt @@ -1,5 +1,7 @@ package com.isolaatti.push_notifications +import okhttp3.MultipartBody +import okhttp3.RequestBody import retrofit2.Call import retrofit2.http.Multipart import retrofit2.http.PUT @@ -9,6 +11,6 @@ interface PushNotificationsApi { @PUT("/api/push_notifications/register_device") @Multipart - fun registerDevice(@Part("token") token: String): Call + fun registerDevice(@Part("token") token: RequestBody): Call } \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/ic_notification.png b/app/src/main/res/drawable-hdpi/ic_notification.png new file mode 100644 index 0000000..4d8cd7f Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_notification.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_notification.png b/app/src/main/res/drawable-mdpi/ic_notification.png new file mode 100644 index 0000000..c11e1c3 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_notification.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_notification.png b/app/src/main/res/drawable-xhdpi/ic_notification.png new file mode 100644 index 0000000..101c1f5 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_notification.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_notification.png b/app/src/main/res/drawable-xxhdpi/ic_notification.png new file mode 100644 index 0000000..97e60b4 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_notification.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_notification.png b/app/src/main/res/drawable-xxxhdpi/ic_notification.png new file mode 100644 index 0000000..909c3da Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_notification.png differ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f1a75a8..4bceca9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -189,4 +189,8 @@ Push notifications Receive notifications to stay informed about your profile activity You won\'t receive notifications. You can change this on your device settings. + Likes notifications + Get notified when someone likes your posts. + %s claps to your post + Tap this notification to go to the post. \ No newline at end of file