From 0f266a1bec6f7be9bbef7edee8fc57d97196c698 Mon Sep 17 00:00:00 2001 From: Kishan Kumar Date: Mon, 30 Jun 2025 19:43:20 +0530 Subject: [PATCH] NTP-66959 | Add android 12 bypass check (#16774) --- .../CustomNotificationHandler.kt | 6 +- .../TimerNotificationRenderer.kt | 2 +- .../TimerNotificationService.kt | 159 +++++++++++++----- 3 files changed, 120 insertions(+), 47 deletions(-) diff --git a/android/navi-common/src/main/java/com/navi/common/pushnotification/CustomNotificationHandler.kt b/android/navi-common/src/main/java/com/navi/common/pushnotification/CustomNotificationHandler.kt index fab272cd8c..8f3c11f9e9 100644 --- a/android/navi-common/src/main/java/com/navi/common/pushnotification/CustomNotificationHandler.kt +++ b/android/navi-common/src/main/java/com/navi/common/pushnotification/CustomNotificationHandler.kt @@ -8,7 +8,6 @@ package com.navi.common.pushnotification import android.content.Context -import android.os.Build import android.os.Bundle import com.google.firebase.messaging.RemoteMessage import com.navi.base.utils.isNotNullAndNotEmpty @@ -29,10 +28,7 @@ object CustomNotificationHandler { } else { when (data[NotificationConstants.TEMPLATE_TYPE]) { TIMER_TEMPLATE -> { - if ( - remoteMessage.priority == RemoteMessage.PRIORITY_HIGH && - Build.VERSION.SDK_INT <= Build.VERSION_CODES.R - ) { + if (remoteMessage.priority == RemoteMessage.PRIORITY_HIGH) { TimerNotificationRenderer.render(context, data) } } diff --git a/android/navi-common/src/main/java/com/navi/common/pushnotification/TimerNotificationRenderer.kt b/android/navi-common/src/main/java/com/navi/common/pushnotification/TimerNotificationRenderer.kt index 38ed2dcdec..153665fb31 100644 --- a/android/navi-common/src/main/java/com/navi/common/pushnotification/TimerNotificationRenderer.kt +++ b/android/navi-common/src/main/java/com/navi/common/pushnotification/TimerNotificationRenderer.kt @@ -410,7 +410,7 @@ object TimerNotificationRenderer { notificationManager?.notify(notificationId /* ID of notification */, notification) } - private fun getChannelId(context: Context): String { + fun getChannelId(context: Context): String { return "${context.packageName}.timer.notification" } diff --git a/android/navi-common/src/main/java/com/navi/common/pushnotification/TimerNotificationService.kt b/android/navi-common/src/main/java/com/navi/common/pushnotification/TimerNotificationService.kt index fb249524b2..c056375611 100644 --- a/android/navi-common/src/main/java/com/navi/common/pushnotification/TimerNotificationService.kt +++ b/android/navi-common/src/main/java/com/navi/common/pushnotification/TimerNotificationService.kt @@ -7,6 +7,7 @@ package com.navi.common.pushnotification +import android.app.NotificationChannel import android.app.NotificationManager import android.app.Service import android.content.Context @@ -16,13 +17,10 @@ import android.os.Build import android.os.Bundle import android.os.CountDownTimer import android.os.IBinder +import androidx.core.app.NotificationCompat import com.navi.base.utils.isNotNull - -enum class TimerState { - STOPPED, - RUNNING, - TERMINATED, -} +import com.navi.common.R +import com.navi.naviwidgets.R as WidgetsR class TimerNotificationService : Service() { companion object { @@ -43,34 +41,113 @@ class TimerNotificationService : Service() { override fun onBind(intent: Intent): IBinder? = null override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - if (intent != null) { - val notificationId = intent.getIntExtra(NotificationConstants.NOTIFICATION_ID, 0) - if ( - !(intent.action == ACTION_TERMINATE && notificationId != foreGroundNotificationId) - ) { - foreGroundNotificationId = - intent.getIntExtra( - NotificationConstants.NOTIFICATION_ID, - TimerNotificationRenderer.getNotificationId(), - ) - bundle = intent.extras ?: Bundle() - bundle.putInt(NotificationConstants.NOTIFICATION_ID, foreGroundNotificationId) - when (intent.action) { - ACTION_PLAY -> { - playTimer(intent.getStringExtra(TIMER_DURATION)?.toLong() ?: -1) - } - ACTION_STOP -> stopTimer() - ACTION_TERMINATE -> terminateTimer() - } - } + // Early return if no intent provided + intent ?: return START_NOT_STICKY + + if (!shouldProcessIntent(intent)) { + return START_NOT_STICKY } + initializeServiceFromIntent(intent) + + processTimerAction(intent) + return START_NOT_STICKY } + private fun shouldProcessIntent(intent: Intent): Boolean { + val notificationId = intent.getIntExtra(NotificationConstants.NOTIFICATION_ID, 0) + return !(intent.action == ACTION_TERMINATE && notificationId != foreGroundNotificationId) + } + + private fun initializeServiceFromIntent(intent: Intent) { + foreGroundNotificationId = + intent.getIntExtra( + NotificationConstants.NOTIFICATION_ID, + TimerNotificationRenderer.getNotificationId(), + ) + + bundle = intent.extras ?: Bundle() + bundle.putInt(NotificationConstants.NOTIFICATION_ID, foreGroundNotificationId) + } + + private fun startForegroundServiceImmediately() { + // CRITICAL: Call startForeground() IMMEDIATELY after getting notification ID + // This prevents ForegroundServiceDidNotStartInTimeException on Android 12+ and + // RemoteServiceException + val loadingNotification = createLoadingNotification(this) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + startForeground( + foreGroundNotificationId, + loadingNotification, + ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE, + ) + } else { + startForeground(foreGroundNotificationId, loadingNotification) + } + } + + private fun processTimerAction(intent: Intent) { + when (intent.action) { + ACTION_PLAY -> { + // Start foreground service immediately + startForegroundServiceImmediately() + val duration = intent.getStringExtra(TIMER_DURATION)?.toLong() ?: -1 + playTimer(duration) + } + + ACTION_STOP -> stopTimer() + ACTION_TERMINATE -> terminateTimer() + } + } + override fun onTaskRemoved(rootIntent: Intent?) { super.onTaskRemoved(rootIntent) } + /** + * Creates a simple, lightweight loading notification for immediate startForeground() call. This + * prevents ForegroundServiceDidNotStartInTimeException by minimizing processing time. + */ + private fun createLoadingNotification(context: Context): android.app.Notification { + val channelId = TimerNotificationRenderer.getChannelId(this) + + // Ensure notification channel exists for Android 8.0+ (API 26+) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + createNotificationChannel(context, channelId) + } + + val smallIcon = + bundle.getString(NotificationConstants.SMALL_ICON)?.toInt() + ?: WidgetsR.drawable.ic_new_navi_logo + + return NotificationCompat.Builder(this, channelId) + .setContentTitle("Loading notification...") + .setContentText("Preparing") + .setSmallIcon(smallIcon) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setCategory(NotificationCompat.CATEGORY_SERVICE) + .setOngoing(true) + .build() + } + + /** + * Creates notification channel for Android 8.0+ (API 26+) if it doesn't exist. Required for + * showing notifications on newer Android versions. + */ + private fun createNotificationChannel(context: Context, channelId: String) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager + val channel = + NotificationChannel( + channelId, + context.applicationContext.getString(R.string.navi_app_custom_channel_name), + NotificationManager.IMPORTANCE_DEFAULT, + ) + notificationManager.createNotificationChannel(channel) + } + } + private fun playTimer(setTime: Long) { if (setTime <= 0) { terminateTimer() @@ -79,18 +156,14 @@ class TimerNotificationService : Service() { this.setTime = setTime secondsRemaining = setTime bundle.putLong(TIME_LEFT, setTime) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - startForeground( - foreGroundNotificationId, - TimerNotificationRenderer.createNotification(this, bundle), - ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE, - ) - } else { - startForeground( - foreGroundNotificationId, - TimerNotificationRenderer.createNotification(this, bundle), - ) - } + + // Update the existing foreground notification with full timer content + // No need to call startForeground() again - service is already in foreground state + val notificationManager = + getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + val fullTimerNotification = TimerNotificationRenderer.createNotification(this, bundle) + notificationManager.notify(foreGroundNotificationId, fullTimerNotification) + TimerNotificationRenderer.updateTimeLeft(this, bundle) timer = @@ -106,7 +179,6 @@ class TimerNotificationService : Service() { } } .start() - state = TimerState.RUNNING } @@ -139,11 +211,16 @@ class TimerNotificationService : Service() { } private fun isValidNotification(notificationId: Int): Boolean { - val notificationManager = - getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager + val notificationManager = getSystemService(NOTIFICATION_SERVICE) as? NotificationManager return notificationManager ?.activeNotifications ?.firstOrNull { it.id == notificationId } .isNotNull() } } + +enum class TimerState { + STOPPED, + RUNNING, + TERMINATED, +}