Skip to content

Commit a00e6e8

Browse files
committed
Address PR Feedback
1 parent 13b10b6 commit a00e6e8

File tree

10 files changed

+434
-233
lines changed

10 files changed

+434
-233
lines changed

app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener;
7171
import org.schabi.newpipe.util.StreamItemAdapter;
7272
import org.schabi.newpipe.util.StreamItemAdapter.StreamInfoWrapper;
73+
import org.schabi.newpipe.util.StreamLabelUtils;
7374
import org.schabi.newpipe.util.ThemeHelper;
7475

7576
import java.io.File;
@@ -1132,7 +1133,8 @@ private void continueSelectedDownload(@NonNull final StoredFileHelper storage) {
11321133
);
11331134
}
11341135

1135-
final String qualityLabel = buildQualityLabel(selectedStream);
1136+
final String qualityLabel =
1137+
StreamLabelUtils.getQualityLabel(requireContext(), selectedStream);
11361138

11371139
DownloadManagerService.startMission(
11381140
context,
@@ -1155,24 +1157,4 @@ private void continueSelectedDownload(@NonNull final StoredFileHelper storage) {
11551157

11561158
dismiss();
11571159
}
1158-
1159-
@Nullable
1160-
private String buildQualityLabel(@NonNull final Stream stream) {
1161-
if (stream instanceof VideoStream) {
1162-
return ((VideoStream) stream).getResolution();
1163-
} else if (stream instanceof AudioStream) {
1164-
final int bitrate = ((AudioStream) stream).getAverageBitrate();
1165-
return bitrate > 0 ? bitrate + "kbps" : null;
1166-
} else if (stream instanceof SubtitlesStream) {
1167-
final SubtitlesStream subtitlesStream = (SubtitlesStream) stream;
1168-
final String language = subtitlesStream.getDisplayLanguageName();
1169-
if (subtitlesStream.isAutoGenerated()) {
1170-
return language + " (" + getString(R.string.caption_auto_generated) + ")";
1171-
}
1172-
return language;
1173-
}
1174-
1175-
final MediaFormat format = stream.getFormat();
1176-
return format != null ? format.getSuffix() : null;
1177-
}
11781160
}

app/src/main/java/org/schabi/newpipe/download/DownloadStatusRepository.kt

Lines changed: 131 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -23,53 +23,77 @@ import us.shandian.giga.service.MissionState
2323
import kotlin.coroutines.resume
2424
import kotlin.coroutines.resumeWithException
2525

26-
sealed interface DownloadStatus {
27-
data object None : DownloadStatus
28-
data class InProgress(val running: Boolean) : DownloadStatus
29-
data class Completed(val info: CompletedDownload) : DownloadStatus
26+
enum class DownloadStage {
27+
Pending,
28+
Running,
29+
Finished
3030
}
3131

32-
data class CompletedDownload(
32+
data class DownloadHandle(
33+
val serviceId: Int,
34+
val url: String,
35+
val streamUid: Long,
36+
val storageUri: Uri?,
37+
val timestamp: Long,
38+
val kind: Char
39+
)
40+
41+
data class DownloadEntry(
42+
val handle: DownloadHandle,
3343
val displayName: String?,
3444
val qualityLabel: String?,
3545
val mimeType: String?,
3646
val fileUri: Uri?,
3747
val parentUri: Uri?,
38-
val fileAvailable: Boolean
48+
val fileAvailable: Boolean,
49+
val stage: DownloadStage
3950
)
4051

4152
object DownloadStatusRepository {
4253

43-
fun observe(context: Context, serviceId: Int, url: String): Flow<DownloadStatus> = callbackFlow {
54+
/**
55+
* Keeps a one-off binding to [DownloadManagerService] alive for as long as the caller stays
56+
* subscribed. We prime the channel with the latest persisted snapshot and then forward every
57+
* mission event emitted by the service-bound handler. Once the consumer cancels the flow we
58+
* make sure to unregister the handler and unbind the service to avoid leaking the connection.
59+
*/
60+
fun observe(context: Context, serviceId: Int, url: String): Flow<List<DownloadEntry>> = callbackFlow {
4461
if (serviceId < 0 || url.isBlank()) {
45-
trySend(DownloadStatus.None)
62+
trySend(emptyList())
4663
close()
4764
return@callbackFlow
4865
}
4966

5067
val appContext = context.applicationContext
5168
val intent = Intent(appContext, DownloadManagerService::class.java)
5269
appContext.startService(intent)
70+
// The download manager service only notifies listeners while a client is bound, so the flow
71+
// keeps a foreground-style binding alive for its entire lifetime. Holding on to
72+
// applicationContext avoids leaking short-lived UI contexts.
5373
var binder: DownloadManagerBinder? = null
5474
var registeredCallback: Handler.Callback? = null
5575

5676
val connection = object : ServiceConnection {
5777
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
5878
val downloadBinder = service as? DownloadManagerBinder
5979
if (downloadBinder == null) {
60-
trySend(DownloadStatus.None)
80+
trySend(emptyList())
6181
appContext.unbindService(this)
6282
close()
6383
return
6484
}
6585
binder = downloadBinder
66-
trySend(downloadBinder.getDownloadStatus(serviceId, url, false).toDownloadStatus())
86+
// First delivery: snapshot persisted on disk so the UI paints immediately even
87+
// before the service emits new events.
88+
trySend(downloadBinder.getDownloadStatuses(serviceId, url, true).toDownloadEntries())
6789

6890
val callback = Handler.Callback { message: Message ->
6991
val mission = message.obj
7092
if (mission.matches(serviceId, url)) {
71-
val snapshot = downloadBinder.getDownloadStatus(serviceId, url, false)
72-
trySend(snapshot.toDownloadStatus())
93+
// Each mission event carries opaque state, so we fetch a fresh snapshot to
94+
// guarantee consistent entries while the download progresses or finishes.
95+
val snapshots = downloadBinder.getDownloadStatuses(serviceId, url, false)
96+
trySend(snapshots.toDownloadEntries())
7397
}
7498
false
7599
}
@@ -80,48 +104,57 @@ object DownloadStatusRepository {
80104
override fun onServiceDisconnected(name: ComponentName?) {
81105
registeredCallback?.let { callback -> binder?.removeMissionEventListener(callback) }
82106
binder = null
83-
trySend(DownloadStatus.None)
107+
trySend(emptyList())
84108
}
85109
}
86110

87111
val bound = appContext.bindService(intent, connection, Context.BIND_AUTO_CREATE)
88112
if (!bound) {
89-
trySend(DownloadStatus.None)
113+
trySend(emptyList())
90114
close()
91115
return@callbackFlow
92116
}
93117

94118
awaitClose {
119+
// When the collector disappears we remove listeners and unbind immediately to avoid
120+
// holding the service forever; the service will rebind on the next subscription.
95121
registeredCallback?.let { callback -> binder?.removeMissionEventListener(callback) }
96122
runCatching { appContext.unbindService(connection) }
97123
}
98124
}
99125

100-
suspend fun refresh(context: Context, serviceId: Int, url: String): DownloadStatus {
101-
if (serviceId < 0 || url.isBlank()) return DownloadStatus.None
126+
suspend fun refresh(context: Context, serviceId: Int, url: String): List<DownloadEntry> {
127+
if (serviceId < 0 || url.isBlank()) return emptyList()
102128
return withBinder(context) { binder ->
103-
binder.getDownloadStatus(serviceId, url, true).toDownloadStatus()
129+
binder.getDownloadStatuses(serviceId, url, true).toDownloadEntries()
104130
}
105131
}
106132

107-
suspend fun deleteFile(context: Context, serviceId: Int, url: String): Boolean {
108-
if (serviceId < 0 || url.isBlank()) return false
133+
suspend fun deleteFile(context: Context, handle: DownloadHandle): Boolean {
134+
if (handle.serviceId < 0 || handle.url.isBlank()) return false
109135
return withBinder(context) { binder ->
110-
binder.deleteFinishedMission(serviceId, url, true)
136+
binder.deleteFinishedMission(handle.serviceId, handle.url, handle.storageUri, handle.timestamp, true)
111137
}
112138
}
113139

114-
suspend fun removeLink(context: Context, serviceId: Int, url: String): Boolean {
115-
if (serviceId < 0 || url.isBlank()) return false
140+
suspend fun removeLink(context: Context, handle: DownloadHandle): Boolean {
141+
if (handle.serviceId < 0 || handle.url.isBlank()) return false
116142
return withBinder(context) { binder ->
117-
binder.deleteFinishedMission(serviceId, url, false)
143+
binder.deleteFinishedMission(handle.serviceId, handle.url, handle.storageUri, handle.timestamp, false)
118144
}
119145
}
120146

147+
/**
148+
* Helper that briefly binds to [DownloadManagerService], executes [block] against its binder
149+
* and tears everything down in one place. All callers should use this to prevent scattering
150+
* ad-hoc bind/unbind logic across the codebase.
151+
*/
121152
private suspend fun <T> withBinder(context: Context, block: (DownloadManagerBinder) -> T): T {
122153
val appContext = context.applicationContext
123154
val intent = Intent(appContext, DownloadManagerService::class.java)
124155
appContext.startService(intent)
156+
// The direct call path still needs the service running long enough to complete the
157+
// binder transaction, so we explicitly start it before establishing the short-lived bind.
125158
return suspendCancellableCoroutine { continuation ->
126159
val connection = object : ServiceConnection {
127160
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
@@ -176,32 +209,83 @@ object DownloadStatusRepository {
176209

177210
@VisibleForTesting
178211
@MainThread
179-
internal fun DownloadManager.DownloadStatusSnapshot?.toDownloadStatus(): DownloadStatus {
180-
if (this == null || state == MissionState.None) {
181-
return DownloadStatus.None
212+
internal fun List<DownloadManager.DownloadStatusSnapshot>.toDownloadEntries(): List<DownloadEntry> {
213+
return buildList {
214+
for (snapshot in this@toDownloadEntries) {
215+
snapshot.toDownloadEntry()?.let { add(it) }
216+
}
182217
}
183-
return when (state) {
184-
MissionState.Pending, MissionState.PendingRunning ->
185-
DownloadStatus.InProgress(state == MissionState.PendingRunning)
186-
MissionState.Finished -> {
187-
val mission = finishedMission
188-
if (mission == null) {
189-
DownloadStatus.None
190-
} else {
191-
val storage = mission.storage
192-
val hasStorage = storage != null && !storage.isInvalid()
193-
val info = CompletedDownload(
194-
displayName = storage?.getName(),
195-
qualityLabel = mission.qualityLabel,
196-
mimeType = if (hasStorage) storage!!.getType() else null,
197-
fileUri = if (hasStorage) storage!!.getUri() else null,
198-
parentUri = if (hasStorage) storage!!.getParentUri() else null,
199-
fileAvailable = fileExists && hasStorage
200-
)
201-
DownloadStatus.Completed(info)
202-
}
218+
}
219+
220+
@VisibleForTesting
221+
@MainThread
222+
internal fun DownloadManager.DownloadStatusSnapshot.toDownloadEntry(): DownloadEntry? {
223+
val stage = when (state) {
224+
MissionState.Pending -> DownloadStage.Pending
225+
MissionState.PendingRunning -> DownloadStage.Running
226+
MissionState.Finished -> DownloadStage.Finished
227+
else -> return null
228+
}
229+
230+
val (metadata, storage) = when (stage) {
231+
DownloadStage.Finished -> finishedMission?.let {
232+
MissionMetadata(
233+
serviceId = it.serviceId,
234+
url = it.source,
235+
streamUid = it.streamUid,
236+
timestamp = it.timestamp,
237+
kind = it.kind,
238+
qualityLabel = it.qualityLabel
239+
) to it.storage
203240
}
204-
else -> DownloadStatus.None
241+
else -> pendingMission?.let {
242+
MissionMetadata(
243+
serviceId = it.serviceId,
244+
url = it.source,
245+
streamUid = it.streamUid,
246+
timestamp = it.timestamp,
247+
kind = it.kind,
248+
qualityLabel = it.qualityLabel
249+
) to it.storage
250+
}
251+
} ?: return null
252+
253+
val hasStorage = storage != null && !storage.isInvalid()
254+
val fileUri = storage?.getUri()
255+
val parentUri = storage?.getParentUri()
256+
257+
val handle = DownloadHandle(
258+
serviceId = metadata.serviceId,
259+
url = metadata.url ?: "",
260+
streamUid = metadata.streamUid,
261+
storageUri = fileUri,
262+
timestamp = metadata.timestamp,
263+
kind = metadata.kind
264+
)
265+
266+
val fileAvailable = when (stage) {
267+
DownloadStage.Finished -> hasStorage && fileExists
268+
DownloadStage.Pending, DownloadStage.Running -> false
205269
}
270+
271+
return DownloadEntry(
272+
handle = handle,
273+
displayName = storage?.getName(),
274+
qualityLabel = metadata.qualityLabel,
275+
mimeType = if (hasStorage) storage.getType() else null,
276+
fileUri = fileUri,
277+
parentUri = parentUri,
278+
fileAvailable = fileAvailable,
279+
stage = stage
280+
)
206281
}
282+
283+
private data class MissionMetadata(
284+
val serviceId: Int,
285+
val url: String?,
286+
val streamUid: Long,
287+
val timestamp: Long,
288+
val kind: Char,
289+
val qualityLabel: String?
290+
)
207291
}

0 commit comments

Comments
 (0)