Skip to content

Commit 735a57d

Browse files
fix & optimize download
1 parent 9ee31f3 commit 735a57d

14 files changed

Lines changed: 401 additions & 133 deletions

File tree

android/src/main/kotlin/project/pipepipe/app/download/DownloadManager.kt

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,6 @@ class DownloadManager(private val context: Context) {
100100
totalBytes = total,
101101
speed = speed
102102
)
103-
104-
// Update notification
105-
DownloadService.updateNotification(context, downloadId, progress, total)
106103
}
107104
},
108105
onStateChange = { status, error ->
@@ -136,6 +133,8 @@ class DownloadManager(private val context: Context) {
136133
if (status.isTerminal()) {
137134
Log.d(TAG, "Download $downloadId finished with status: $status")
138135

136+
val download = DatabaseOperations.getDownloadById(downloadId)
137+
139138
// Remove from active workers
140139
activeWorkers.remove(downloadId)
141140
updateActiveDownloadIds()
@@ -145,14 +144,18 @@ class DownloadManager(private val context: Context) {
145144

146145
// Stop service if no more active downloads
147146
if (activeWorkers.isEmpty()) {
148-
DownloadService.stop(context)
147+
DownloadService.updateForegroundState(context, false)
149148
}
150149

151150
// Show completion notification
152151
if (status == DownloadStatus.COMPLETED) {
153-
DownloadService.showCompletionNotification(context, downloadId)
152+
download?.title?.let { title ->
153+
DownloadService.showCompletionNotification(context, title)
154+
}
154155
} else if (status == DownloadStatus.FAILED) {
155-
DownloadService.showErrorNotification(context, downloadId, error)
156+
download?.title?.let { title ->
157+
DownloadService.showErrorNotification(context, title, error)
158+
}
156159
}
157160
}
158161
}
@@ -162,6 +165,9 @@ class DownloadManager(private val context: Context) {
162165
activeWorkers[downloadId] = worker
163166
updateActiveDownloadIds()
164167

168+
// Update foreground state
169+
DownloadService.updateForegroundState(context, true)
170+
165171
// Start the worker
166172
scope.launch {
167173
worker.execute()
@@ -204,9 +210,9 @@ class DownloadManager(private val context: Context) {
204210
// Start next queued download
205211
startNextInQueue()
206212

207-
// Stop service if no more active downloads
213+
// Update foreground state
208214
if (activeWorkers.isEmpty()) {
209-
DownloadService.stop(context)
215+
DownloadService.updateForegroundState(context, false)
210216
}
211217
}
212218
}
@@ -241,19 +247,16 @@ class DownloadManager(private val context: Context) {
241247
activeWorkers.remove(downloadId)
242248
updateActiveDownloadIds()
243249

244-
// Cancel progress notification
245-
DownloadService.cancelProgressNotification(context, downloadId)
246-
247250
scope.launch {
248251
// Delete from database
249252
DatabaseOperations.deleteDownload(downloadId)
250253

251254
// Start next queued download
252255
startNextInQueue()
253256

254-
// Stop service if no more active downloads
257+
// Update foreground state
255258
if (activeWorkers.isEmpty()) {
256-
DownloadService.stop(context)
259+
DownloadService.updateForegroundState(context, false)
257260
}
258261
}
259262
}
Lines changed: 134 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,85 @@
11
package project.pipepipe.app.service
22

3-
import android.app.Notification
43
import android.app.NotificationChannel
54
import android.app.NotificationManager
65
import android.app.PendingIntent
76
import android.app.Service
87
import android.content.Context
98
import android.content.Intent
9+
import android.os.Build
1010
import android.os.IBinder
1111
import androidx.core.app.NotificationCompat
12+
import androidx.core.app.ServiceCompat
1213
import 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
1816
import 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
*/
2423
class 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

Comments
 (0)