11package project.pipepipe.app.service
22
3- import android.app.Notification
43import android.app.NotificationChannel
54import android.app.NotificationManager
65import android.app.PendingIntent
76import android.app.Service
87import android.content.Context
98import android.content.Intent
9+ import android.os.Build
1010import android.os.IBinder
1111import androidx.core.app.NotificationCompat
12+ import androidx.core.app.ServiceCompat
1213import androidx.core.content.ContextCompat
13- import kotlinx.coroutines.CoroutineScope
14- import kotlinx.coroutines.Dispatchers
15- import kotlinx.coroutines.SupervisorJob
16- import kotlinx.coroutines.launch
17- import project.pipepipe.app.R
14+ import kotlinx.coroutines.runBlocking
15+ import project.pipepipe.app.MR
1816import project.pipepipe.app.database.DatabaseOperations
17+ import dev.icerock.moko.resources.desc.desc
1918
2019/* *
2120 * Foreground service for managing downloads
22- * Keeps the app alive while downloads are in progress
21+ * Keeps app alive while downloads are in progress
2322 */
2423class DownloadService : Service () {
2524
26- private val scope = CoroutineScope (SupervisorJob () + Dispatchers .Main )
27- private val notificationManager by lazy {
28- getSystemService(Context .NOTIFICATION_SERVICE ) as NotificationManager
29- }
25+ private var isForeground = false
3026
3127 override fun onCreate () {
3228 super .onCreate()
29+ instance = this
3330 createNotificationChannel()
3431
35- // Start as foreground service
36- val notification = createForegroundNotification()
37- startForeground(NOTIFICATION_ID , notification)
32+ // Start foreground immediately to satisfy system requirement
33+ val notification = NotificationCompat .Builder (this , CHANNEL_ID )
34+ .setContentTitle(MR .strings.downloads.desc().toString(this ))
35+ .setContentText(MR .strings.notification_downloads_in_progress.desc().toString(this ))
36+ .setSmallIcon(android.R .drawable.stat_sys_download)
37+ .setPriority(NotificationCompat .PRIORITY_LOW )
38+ .setOngoing(true )
39+ .build()
40+ startForeground(FOREGROUND_NOTIFICATION_ID , notification)
41+ isForeground = true
42+
43+ // Check if there are actually active downloads, stop foreground if not
44+ val activeDownloads = runBlocking { DatabaseOperations .getActiveDownloads() }
45+ if (activeDownloads.isEmpty()) {
46+ stopForeground()
47+ }
3848 }
3949
4050 override fun onStartCommand (intent : Intent ? , flags : Int , startId : Int ): Int {
41- // Service is controlled by DownloadManager, just maintain foreground state
42- return START_STICKY
51+ // Handle notification actions
52+ intent?.action?.let { action ->
53+ if (action == ACTION_RESET_DOWNLOAD_FINISHED || action == ACTION_OPEN_DOWNLOADS_FINISHED ) {
54+ DownloadNotificationManager .reset()
55+ }
56+ }
57+ return START_NOT_STICKY
4358 }
4459
45- override fun onBind (intent : Intent ? ): IBinder ? {
46- return null
47- }
60+ override fun onBind (intent : Intent ? ): IBinder ? = null
4861
4962 override fun onDestroy () {
5063 super .onDestroy()
51- notificationManager.cancel(NOTIFICATION_ID )
64+ instance = null
65+ stopForeground()
5266 }
5367
5468 private fun createNotificationChannel () {
5569 val channel = NotificationChannel (
5670 CHANNEL_ID ,
57- " Downloads " ,
71+ MR .strings.downloads.desc().toString( this ) ,
5872 NotificationManager .IMPORTANCE_LOW
5973 ).apply {
6074 description = " Download progress notifications"
6175 setShowBadge(false )
6276 }
77+ val notificationManager = getSystemService(Context .NOTIFICATION_SERVICE ) as NotificationManager
6378 notificationManager.createNotificationChannel(channel)
6479
65- // Create channel for completion notifications
6680 val completionChannel = NotificationChannel (
6781 COMPLETION_CHANNEL_ID ,
68- " Download Completion " ,
82+ MR .strings.download_completion_channel_name.desc().toString( this ) ,
6983 NotificationManager .IMPORTANCE_DEFAULT
7084 ).apply {
7185 description = " Notifications for completed downloads"
@@ -74,102 +88,128 @@ class DownloadService : Service() {
7488 notificationManager.createNotificationChannel(completionChannel)
7589 }
7690
77- private fun createForegroundNotification (): Notification {
78- return NotificationCompat .Builder (this , CHANNEL_ID )
79- .setContentTitle(" Downloads" )
80- .setContentText(" Download in progress..." )
81- .setSmallIcon(android.R .drawable.stat_sys_download)
82- .setPriority(NotificationCompat .PRIORITY_LOW )
83- .setOngoing(true )
84- .build()
91+ private fun updateForegroundState (hasActiveDownloads : Boolean ) {
92+ android.util.Log .d(" DownloadService" , " updateForegroundState: hasActiveDownloads=$hasActiveDownloads , isForeground=$isForeground " )
93+ if (hasActiveDownloads == isForeground) return
94+
95+ if (hasActiveDownloads) {
96+ android.util.Log .d(" DownloadService" , " Starting foreground" )
97+ val notification = NotificationCompat .Builder (this , CHANNEL_ID )
98+ .setContentTitle(MR .strings.downloads.desc().toString(this ))
99+ .setContentText(MR .strings.notification_downloads_in_progress.desc().toString(this ))
100+ .setSmallIcon(android.R .drawable.stat_sys_download)
101+ .setPriority(NotificationCompat .PRIORITY_LOW )
102+ .setOngoing(true )
103+ .build()
104+ startForeground(FOREGROUND_NOTIFICATION_ID , notification)
105+ isForeground = true
106+ } else {
107+ android.util.Log .d(" DownloadService" , " Stopping foreground" )
108+ stopForeground()
109+ }
110+ }
111+
112+ private fun stopForeground () {
113+ if (isForeground) {
114+ ServiceCompat .stopForeground(this , ServiceCompat .STOP_FOREGROUND_REMOVE )
115+ isForeground = false
116+ stopSelf()
117+ }
85118 }
86119
87120 companion object {
88121 private const val CHANNEL_ID = " download_channel"
89122 private const val COMPLETION_CHANNEL_ID = " download_completion_channel"
90- private const val NOTIFICATION_ID = 3001
91- private const val PROGRESS_NOTIFICATION_BASE_ID = 3100
123+ private const val FOREGROUND_NOTIFICATION_ID = 3001
124+ private const val COMPLETION_NOTIFICATION_ID = 3002
125+ private const val ERROR_NOTIFICATION_BASE_ID = 3100
126+
127+ private const val ACTION_RESET_DOWNLOAD_FINISHED = " project.pipepipe.reset_download_finished"
128+ private const val ACTION_OPEN_DOWNLOADS_FINISHED = " project.pipepipe.open_downloads_finished"
129+
130+ private var instance: DownloadService ? = null
92131
93132 fun start (context : Context ) {
94133 val intent = Intent (context, DownloadService ::class .java)
95134 ContextCompat .startForegroundService(context, intent)
96135 }
97136
98- fun stop (context : Context ) {
99- val intent = Intent (context, DownloadService :: class .java )
100- context.stopService(intent )
137+ fun updateForegroundState (context : Context , hasActiveDownloads : Boolean ) {
138+ android.util. Log .d( " DownloadService " , " Companion updateForegroundState: hasActiveDownloads= $hasActiveDownloads , instance= $instance " )
139+ instance?.updateForegroundState(hasActiveDownloads )
101140 }
102141
103- fun updateNotification (context : Context , downloadId : Long , progress : Float , totalBytes : Long? ) {
104- val scope = CoroutineScope (Dispatchers .IO )
105- scope.launch {
106- val download = DatabaseOperations .getDownloadById(downloadId) ? : return @launch
107-
108- val notificationManager = context.getSystemService(Context .NOTIFICATION_SERVICE ) as NotificationManager
109-
110- val notification = NotificationCompat .Builder (context, CHANNEL_ID )
111- .setContentTitle(download.title)
112- .setContentText(" ${(progress * 100 ).toInt()} % - ${download.quality} " )
113- .setSmallIcon(android.R .drawable.stat_sys_download)
114- .setProgress(100 , (progress * 100 ).toInt(), false )
115- .setOngoing(true )
116- .setPriority(NotificationCompat .PRIORITY_LOW )
117- .build()
142+ fun showCompletionNotification (context : Context , title : String ) {
143+ val notificationManager = context.getSystemService(Context .NOTIFICATION_SERVICE ) as NotificationManager
118144
119- notificationManager.notify(PROGRESS_NOTIFICATION_BASE_ID + downloadId.toInt(), notification)
120- }
145+ val count = DownloadNotificationManager .increment()
146+ DownloadNotificationManager .append(title)
147+
148+ val builder = NotificationCompat .Builder (context, COMPLETION_CHANNEL_ID )
149+ .setSmallIcon(android.R .drawable.stat_sys_download_done)
150+ .setAutoCancel(true )
151+ .setDeleteIntent(makePendingIntent(context, ACTION_RESET_DOWNLOAD_FINISHED ))
152+ .setContentIntent(makePendingIntent(context, ACTION_OPEN_DOWNLOADS_FINISHED ))
153+ .setContentTitle(MR .strings.notification_download_count.desc().toString(context).format(count))
154+ .setContentText(DownloadNotificationManager .getList())
155+ .setStyle(
156+ NotificationCompat .BigTextStyle ()
157+ .setBigContentTitle(MR .strings.notification_download_count.desc().toString(context).format(count))
158+ .bigText(DownloadNotificationManager .getList())
159+ )
160+
161+ notificationManager.notify(COMPLETION_NOTIFICATION_ID , builder.build())
121162 }
122163
123- fun showCompletionNotification (context : Context , downloadId : Long ) {
124- val scope = CoroutineScope (Dispatchers .IO )
125- scope.launch {
126- val download = DatabaseOperations .getDownloadById(downloadId) ? : return @launch
127-
128- val notificationManager = context.getSystemService(Context .NOTIFICATION_SERVICE ) as NotificationManager
164+ fun showErrorNotification (context : Context , title : String , error : String? ) {
165+ val notificationManager = context.getSystemService(Context .NOTIFICATION_SERVICE ) as NotificationManager
129166
130- // Cancel progress notification
131- notificationManager.cancel(PROGRESS_NOTIFICATION_BASE_ID + downloadId.toInt())
167+ val builder = NotificationCompat .Builder (context, COMPLETION_CHANNEL_ID )
168+ .setContentTitle(MR .strings.download_failed.desc().toString(context))
169+ .setContentText(title)
170+ .setStyle(NotificationCompat .BigTextStyle ().bigText(error ? : MR .strings.download_unknown_error.desc().toString(context)))
171+ .setSmallIcon(android.R .drawable.stat_notify_error)
172+ .setPriority(NotificationCompat .PRIORITY_DEFAULT )
173+ .setAutoCancel(true )
174+ .build()
132175
133- // Show completion notification
134- val notification = NotificationCompat .Builder (context, COMPLETION_CHANNEL_ID )
135- .setContentTitle(" Download Complete" )
136- .setContentText(download.title)
137- .setSmallIcon(android.R .drawable.stat_sys_download_done)
138- .setPriority(NotificationCompat .PRIORITY_DEFAULT )
139- .setAutoCancel(true )
140- .build()
176+ notificationManager.notify(ERROR_NOTIFICATION_BASE_ID + title.hashCode(), builder)
177+ }
141178
142- notificationManager.notify(PROGRESS_NOTIFICATION_BASE_ID + downloadId.toInt(), notification)
179+ private fun makePendingIntent (context : Context , action : String ): PendingIntent {
180+ val intent = Intent (context, DownloadService ::class .java).setAction(action)
181+ val flags = if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .M ) {
182+ PendingIntent .FLAG_IMMUTABLE or PendingIntent .FLAG_UPDATE_CURRENT
183+ } else {
184+ PendingIntent .FLAG_UPDATE_CURRENT
143185 }
186+ return PendingIntent .getService(context, action.hashCode(), intent, flags)
144187 }
188+ }
189+ }
145190
146- fun showErrorNotification (context : Context , downloadId : Long , error : String? ) {
147- val scope = CoroutineScope (Dispatchers .IO )
148- scope.launch {
149- val download = DatabaseOperations .getDownloadById(downloadId) ? : return @launch
150-
151- val notificationManager = context.getSystemService(Context .NOTIFICATION_SERVICE ) as NotificationManager
152-
153- // Cancel progress notification
154- notificationManager.cancel(PROGRESS_NOTIFICATION_BASE_ID + downloadId.toInt())
191+ object DownloadNotificationManager {
192+ private var count = 0
193+ private val list = StringBuilder ()
155194
156- // Show error notification
157- val notification = NotificationCompat .Builder (context, COMPLETION_CHANNEL_ID )
158- .setContentTitle(" Download Failed" )
159- .setContentText(download.title)
160- .setStyle(NotificationCompat .BigTextStyle ().bigText(error ? : " Unknown error" ))
161- .setSmallIcon(android.R .drawable.stat_notify_error)
162- .setPriority(NotificationCompat .PRIORITY_DEFAULT )
163- .setAutoCancel(true )
164- .build()
195+ fun increment (): Int {
196+ count++
197+ return count
198+ }
165199
166- notificationManager.notify(PROGRESS_NOTIFICATION_BASE_ID + downloadId.toInt(), notification)
167- }
168- }
200+ fun reset () {
201+ count = 0
202+ list.clear()
203+ }
169204
170- fun cancelProgressNotification (context : Context , downloadId : Long ) {
171- val notificationManager = context.getSystemService(Context .NOTIFICATION_SERVICE ) as NotificationManager
172- notificationManager.cancel(PROGRESS_NOTIFICATION_BASE_ID + downloadId.toInt())
205+ fun append (title : String ) {
206+ if (list.isEmpty()) {
207+ list.append(title)
208+ } else {
209+ list.append(' \n ' )
210+ list.append(title)
173211 }
174212 }
213+
214+ fun getList (): String = list.toString()
175215}
0 commit comments