Compare commits

..

6 Commits

Author SHA1 Message Date
Fredrik Burmester
24d04c1003 fix: BOOT_COMPLETED runtime protection 2026-01-05 21:32:41 +01:00
Fredrik Burmester
7da52441ab chore: version 2026-01-05 21:32:41 +01:00
Fredrik Burmester
70268e6120 fix: use vlc build with 16kb page size 2026-01-05 21:32:41 +01:00
Fredrik Burmester
96fbb9fe1f fix: target version 35 2026-01-05 21:32:40 +01:00
Fredrik Burmester
3b104b91fc chore: version 2026-01-05 21:32:40 +01:00
Fredrik Burmester
e4134d6f9a fix: item content header button colors 2026-01-05 21:32:40 +01:00
5 changed files with 91 additions and 27 deletions

View File

@@ -34,7 +34,7 @@
},
"android": {
"jsEngine": "hermes",
"versionCode": 90,
"versionCode": 91,
"adaptiveIcon": {
"foregroundImage": "./assets/images/icon-android-plain.png",
"monochromeImage": "./assets/images/icon-android-themed.png",

View File

@@ -5,42 +5,92 @@ import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Intent
import android.content.pm.ServiceInfo
import android.os.Binder
import android.os.Build
import android.os.IBinder
import android.os.SystemClock
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
class DownloadService : Service() {
private val TAG = "DownloadService"
private val NOTIFICATION_ID = 1001
private val CHANNEL_ID = "download_channel"
// Time threshold to detect if we're in boot context (10 minutes after boot)
private val BOOT_THRESHOLD_MS = 10 * 60 * 1000L
private val binder = DownloadServiceBinder()
private var activeDownloadCount = 0
private var currentDownloadTitle = "Preparing download..."
private var currentProgress = 0
private var isForegroundStarted = false
inner class DownloadServiceBinder : Binder() {
fun getService(): DownloadService = this@DownloadService
}
override fun onCreate() {
super.onCreate()
Log.d(TAG, "DownloadService created")
createNotificationChannel()
}
override fun onBind(intent: Intent?): IBinder {
Log.d(TAG, "DownloadService bound")
return binder
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d(TAG, "DownloadService started")
startForeground(NOTIFICATION_ID, createNotification())
// On Android 15+, dataSync foreground services cannot be started from BOOT_COMPLETED context
// Check if we're likely in a boot context and skip foreground start if so
if (Build.VERSION.SDK_INT >= 35 && isLikelyBootContext()) {
Log.w(TAG, "Skipping foreground start - likely boot context on Android 15+")
stopSelf()
return START_NOT_STICKY
}
startForegroundSafely()
return START_STICKY
}
/**
* Check if we're likely in a boot context by checking system uptime.
* If the system has been up for less than the threshold, we might be in boot context.
*/
private fun isLikelyBootContext(): Boolean {
val uptimeMs = SystemClock.elapsedRealtime()
return uptimeMs < BOOT_THRESHOLD_MS
}
/**
* Start foreground service safely with proper service type for Android 14+
*/
private fun startForegroundSafely() {
if (isForegroundStarted) return
try {
if (Build.VERSION.SDK_INT >= 34) {
ServiceCompat.startForeground(
this,
NOTIFICATION_ID,
createNotification(),
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
)
} else {
startForeground(NOTIFICATION_ID, createNotification())
}
isForegroundStarted = true
} catch (e: Exception) {
Log.e(TAG, "Failed to start foreground service", e)
// If we can't start foreground, stop the service
stopSelf()
}
}
override fun onDestroy() {
Log.d(TAG, "DownloadService destroyed")
@@ -86,7 +136,7 @@ class DownloadService : Service() {
activeDownloadCount++
Log.d(TAG, "Download started, active count: $activeDownloadCount")
if (activeDownloadCount == 1) {
startForeground(NOTIFICATION_ID, createNotification())
startForegroundSafely()
}
}
@@ -94,7 +144,10 @@ class DownloadService : Service() {
activeDownloadCount = maxOf(0, activeDownloadCount - 1)
Log.d(TAG, "Download stopped, active count: $activeDownloadCount")
if (activeDownloadCount == 0) {
stopForeground(STOP_FOREGROUND_REMOVE)
if (isForegroundStarted) {
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
isForegroundStarted = false
}
stopSelf()
}
}

View File

@@ -35,7 +35,7 @@ android {
}
dependencies {
implementation 'org.videolan.android:libvlc-all:4.0.0-eap23'
implementation 'io.github.mengzhidaren:vlc-android-sdk:3.6.3'
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
}

View File

@@ -63,16 +63,16 @@ class VlcPlayerModule : Module() {
view.seekTo(time)
}
AsyncFunction("setAudioTrack") { view: VlcPlayerView, trackId: String ->
view.setAudioTrack(trackId)
AsyncFunction("setAudioTrack") { view: VlcPlayerView, trackIndex: Int ->
view.setAudioTrack(trackIndex)
}
AsyncFunction("getAudioTracks") { view: VlcPlayerView ->
view.getAudioTracks()
}
AsyncFunction("setSubtitleTrack") { view: VlcPlayerView, trackId: String ->
view.setSubtitleTrack(trackId)
AsyncFunction("setSubtitleTrack") { view: VlcPlayerView, trackIndex: Int ->
view.setSubtitleTrack(trackIndex)
}
AsyncFunction("getSubtitleTracks") { view: VlcPlayerView ->

View File

@@ -234,7 +234,6 @@ class VlcPlayerView(context: Context, appContext: AppContext) : ExpoView(context
libVLC = LibVLC(context, initOptions)
mediaPlayer = MediaPlayer(libVLC)
mediaPlayer?.attachViews(videoLayout, null, false, false)
mediaPlayer?.setVideoScale(MediaPlayer.ScaleType.SURFACE_BEST_FIT)
mediaPlayer?.setEventListener(this)
log.debug("Loading network file: $uri")
@@ -295,26 +294,38 @@ class VlcPlayerView(context: Context, appContext: AppContext) : ExpoView(context
}
}
fun setAudioTrack(trackId: String) {
// TODO: VLC 4.0 API - need to find correct method
log.debug("setAudioTrack called with $trackId - not yet implemented for VLC 4.0")
fun setAudioTrack(trackIndex: Int) {
mediaPlayer?.setAudioTrack(trackIndex)
}
fun getAudioTracks(): List<Map<String, Any>>? {
// TODO: VLC 4.0 API - need to find correct method
log.debug("getAudioTracks - not yet implemented for VLC 4.0")
return emptyList()
log.debug("getAudioTracks ${mediaPlayer?.audioTracks}")
val trackDescriptions = mediaPlayer?.audioTracks ?: return null
return trackDescriptions.map { trackDescription ->
mapOf("name" to trackDescription.name, "index" to trackDescription.id)
}
}
fun setSubtitleTrack(trackId: String) {
// TODO: VLC 4.0 API - need to find correct method
log.debug("setSubtitleTrack called with $trackId - not yet implemented for VLC 4.0")
fun setSubtitleTrack(trackIndex: Int) {
mediaPlayer?.setSpuTrack(trackIndex)
}
// fun getSubtitleTracks(): List<Map<String, Any>>? {
// return mediaPlayer?.getSpuTracks()?.map { trackDescription ->
// mapOf("name" to trackDescription.name, "index" to trackDescription.id)
// }
// }
fun getSubtitleTracks(): List<Map<String, Any>>? {
// TODO: VLC 4.0 API - need to find correct method
log.debug("getSubtitleTracks - not yet implemented for VLC 4.0")
return emptyList()
val subtitleTracks = mediaPlayer?.spuTracks?.map { trackDescription ->
mapOf("name" to trackDescription.name, "index" to trackDescription.id)
}
// Debug statement to print the result
log.debug("Subtitle Tracks: $subtitleTracks")
return subtitleTracks
}
fun setSubtitleURL(subtitleURL: String, name: String) {