mirror of
https://github.com/zoriya/react-native-omni.git
synced 2026-08-16 02:44:51 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f6d544591 | ||
|
|
456f903f97 | ||
|
|
af3e96de78 | ||
|
|
fe51950bcf | ||
|
|
cd4ad50604 |
+9
-10
@@ -5,7 +5,7 @@ buildscript {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath "com.android.tools.build:gradle:8.8.0"
|
||||
classpath "com.android.tools.build:gradle:9.2.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,16 +143,15 @@ dependencies {
|
||||
// Add a dependency on NitroModules
|
||||
implementation project(":react-native-nitro-modules")
|
||||
|
||||
implementation("org.videolan.android:libvlc-all:4.0.0-eap24")
|
||||
implementation("androidx.media3:media3-common:1.4.1")
|
||||
implementation("androidx.media3:media3-session:1.4.1")
|
||||
implementation("androidx.media3:media3-ui:1.4.1")
|
||||
implementation 'androidx.media3:media3-exoplayer:1.10.0'
|
||||
implementation("androidx.media3:media3-exoplayer-hls:1.10.0")
|
||||
|
||||
implementation("androidx.media3:media3-cast:1.10.0")
|
||||
implementation("org.videolan.android:libvlc-all:4.0.0-eap29")
|
||||
implementation("androidx.media3:media3-common:1.11.0")
|
||||
implementation("androidx.media3:media3-session:1.11.0")
|
||||
implementation("androidx.media3:media3-ui:1.11.0")
|
||||
implementation 'androidx.media3:media3-exoplayer:1.11.0'
|
||||
implementation("androidx.media3:media3-exoplayer-hls:1.11.0")
|
||||
implementation("androidx.media3:media3-cast:1.11.0")
|
||||
implementation("com.google.android.gms:play-services-cast-framework:22.3.1")
|
||||
implementation("androidx.mediarouter:mediarouter:1.7.0")
|
||||
implementation("androidx.mediarouter:mediarouter:1.8.1")
|
||||
}
|
||||
|
||||
if (isNewArchitectureEnabled()) {
|
||||
|
||||
@@ -5,12 +5,9 @@ import androidx.media3.common.C
|
||||
import androidx.media3.common.C.TRACK_TYPE_AUDIO
|
||||
import androidx.media3.common.C.TRACK_TYPE_TEXT
|
||||
import androidx.media3.common.C.TRACK_TYPE_VIDEO
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.PlaybackException
|
||||
import androidx.media3.common.PlaybackParameters
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.Player.MEDIA_ITEM_TRANSITION_REASON_AUTO
|
||||
import androidx.media3.common.Player.MEDIA_ITEM_TRANSITION_REASON_SEEK
|
||||
import androidx.media3.common.Player.STATE_BUFFERING
|
||||
import androidx.media3.common.Player.STATE_ENDED
|
||||
import androidx.media3.common.Player.STATE_IDLE
|
||||
@@ -59,7 +56,6 @@ class EventMap(private val tracks: TrackProvider) : HybridOmniEventMapSpec(), Pl
|
||||
private val castStatusListeners = mutableSetOf<(CastStatus) -> Unit>()
|
||||
private val sourceListeners = mutableSetOf<(Source?) -> Unit>()
|
||||
|
||||
private var lastMediaItemIndex = 0
|
||||
private var lastRendition: Rendition? = null
|
||||
private var lastRenditions: Array<Rendition>? = null
|
||||
private var lastIsAutoQuality: Boolean? = null
|
||||
@@ -73,7 +69,6 @@ class EventMap(private val tracks: TrackProvider) : HybridOmniEventMapSpec(), Pl
|
||||
_player?.removeListener(this)
|
||||
_player = value
|
||||
value.addListener(this)
|
||||
lastMediaItemIndex = value.currentMediaItemIndex
|
||||
lastRendition = null
|
||||
lastRenditions = null
|
||||
lastIsAutoQuality = null
|
||||
@@ -116,6 +111,14 @@ class EventMap(private val tracks: TrackProvider) : HybridOmniEventMapSpec(), Pl
|
||||
sourceListeners.forEach { it(source) }
|
||||
}
|
||||
|
||||
fun emitPrev() {
|
||||
onPrevListeners.forEach { it() }
|
||||
}
|
||||
|
||||
fun emitNext() {
|
||||
onNextListeners.forEach { it() }
|
||||
}
|
||||
|
||||
private fun selectedTrack(trackType: Int): Track? {
|
||||
val list = when (trackType) {
|
||||
TRACK_TYPE_VIDEO -> tracks.videos
|
||||
@@ -278,20 +281,6 @@ class EventMap(private val tracks: TrackProvider) : HybridOmniEventMapSpec(), Pl
|
||||
playerStatusListeners.forEach { it(PlayerStatus.ERROR) }
|
||||
}
|
||||
|
||||
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
||||
if (reason != MEDIA_ITEM_TRANSITION_REASON_AUTO && reason != MEDIA_ITEM_TRANSITION_REASON_SEEK) {
|
||||
lastMediaItemIndex = player.currentMediaItemIndex
|
||||
return
|
||||
}
|
||||
val newIndex = player.currentMediaItemIndex
|
||||
if (newIndex < lastMediaItemIndex) {
|
||||
onPrevListeners.forEach { it() }
|
||||
} else if (newIndex > lastMediaItemIndex) {
|
||||
onNextListeners.forEach { it() }
|
||||
}
|
||||
lastMediaItemIndex = newIndex
|
||||
}
|
||||
|
||||
override fun onPositionDiscontinuity(
|
||||
oldPosition: Player.PositionInfo,
|
||||
newPosition: Player.PositionInfo,
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package dev.zoriya.omni
|
||||
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.ForwardingSimpleBasePlayer
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
class NavigationPlayer(
|
||||
player: Player,
|
||||
private val hasPrev: () -> Boolean,
|
||||
private val hasNext: () -> Boolean,
|
||||
private val onPrev: () -> Unit,
|
||||
private val onNext: () -> Unit,
|
||||
) : ForwardingSimpleBasePlayer(player) {
|
||||
override fun getState(): State {
|
||||
val state = super.getState()
|
||||
return state.buildUpon()
|
||||
.setAvailableCommands(
|
||||
state.availableCommands.buildUpon()
|
||||
.addIf(Player.COMMAND_SEEK_TO_PREVIOUS, hasPrev())
|
||||
.addIf(Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM, hasPrev())
|
||||
.addIf(Player.COMMAND_SEEK_TO_NEXT, hasNext())
|
||||
.addIf(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, hasNext())
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun handleSeek(
|
||||
mediaItemIndex: Int,
|
||||
positionMs: Long,
|
||||
seekCommand: Int,
|
||||
): ListenableFuture<*> = when (seekCommand) {
|
||||
Player.COMMAND_SEEK_TO_PREVIOUS,
|
||||
Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM -> {
|
||||
onPrev()
|
||||
Futures.immediateVoidFuture()
|
||||
}
|
||||
|
||||
Player.COMMAND_SEEK_TO_NEXT,
|
||||
Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM -> {
|
||||
onNext()
|
||||
Futures.immediateVoidFuture()
|
||||
}
|
||||
|
||||
else -> super.handleSeek(mediaItemIndex, positionMs, seekCommand)
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package dev.zoriya.omni
|
||||
import android.content.Context
|
||||
import com.google.android.gms.cast.CastMediaControlIntent
|
||||
import com.google.android.gms.cast.framework.CastOptions
|
||||
import com.google.android.gms.cast.framework.media.CastMediaOptions
|
||||
import com.google.android.gms.cast.framework.OptionsProvider
|
||||
import com.google.android.gms.cast.framework.SessionProvider
|
||||
|
||||
@@ -11,9 +12,15 @@ class OmniCastOptionsProvider : OptionsProvider {
|
||||
val appId = OmniPlayer.receiverApplicationId
|
||||
?: CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID
|
||||
|
||||
val mediaOptions = CastMediaOptions.Builder()
|
||||
.setMediaSessionEnabled(false)
|
||||
.setNotificationOptions(null)
|
||||
.build()
|
||||
|
||||
return CastOptions.Builder()
|
||||
.setReceiverApplicationId(appId)
|
||||
.setShowSystemOutputSwitcherOnCastIconClick(true)
|
||||
.setCastMediaOptions(mediaOptions)
|
||||
.build()
|
||||
}
|
||||
|
||||
|
||||
@@ -60,13 +60,18 @@ class OmniCastTrackSelector : CastTrackSelector() {
|
||||
val overrideMatch = indices.firstOrNull { i ->
|
||||
overrides.keys.any { it.id == groups[i].id }
|
||||
}
|
||||
val match = indices.firstOrNull { i ->
|
||||
val matchesLabel = { i: Int ->
|
||||
val name: String? = tracks[i].name
|
||||
val language: String? = tracks[i].language
|
||||
(labels.isNotEmpty() && name != null && name in labels) ||
|
||||
(languages.isNotEmpty() && language != null &&
|
||||
Util.normalizeLanguageCode(language) in languages)
|
||||
labels.isNotEmpty() && name != null && name in labels
|
||||
}
|
||||
val matchesLanguage = { i: Int ->
|
||||
val language: String? = tracks[i].language
|
||||
languages.isNotEmpty() && language != null &&
|
||||
Util.normalizeLanguageCode(language) in languages
|
||||
}
|
||||
val match = indices.firstOrNull { matchesLabel(it) && matchesLanguage(it) }
|
||||
?: indices.firstOrNull { matchesLabel(it) }
|
||||
?: indices.firstOrNull { matchesLanguage(it) }
|
||||
val chosen = when {
|
||||
overrideMatch != null -> overrideMatch
|
||||
match != null -> match
|
||||
|
||||
@@ -24,8 +24,15 @@ class OmniMediaItemConverter : MediaItemConverter {
|
||||
builder.setMediaMetadata(
|
||||
androidx.media3.common.MediaMetadata.Builder()
|
||||
.setTitle(md.getString(CastMediaMetadata.KEY_TITLE))
|
||||
.setArtist(md.getString(CastMediaMetadata.KEY_ARTIST))
|
||||
.setAlbumTitle(md.getString(CastMediaMetadata.KEY_ALBUM_TITLE))
|
||||
.setArtist(
|
||||
md.getString(CastMediaMetadata.KEY_ARTIST)
|
||||
?: md.getString(CastMediaMetadata.KEY_SUBTITLE)
|
||||
)
|
||||
.setAlbumTitle(
|
||||
md.getString(CastMediaMetadata.KEY_ALBUM_TITLE)
|
||||
?: md.getString(CastMediaMetadata.KEY_STUDIO)
|
||||
)
|
||||
.setArtworkUri(md.images.firstOrNull()?.url)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -99,8 +99,27 @@ class OmniPlayer(
|
||||
override val eventMap = EventMap(this)
|
||||
|
||||
private var castContext: CastContext? = null
|
||||
private val castStateListener =
|
||||
CastStateListener { eventMap.emitCastStatus(computeCastStatus()) }
|
||||
private val castStateListener = CastStateListener {
|
||||
eventMap.emitCastStatus(computeCastStatus())
|
||||
listenToReceiver()
|
||||
}
|
||||
|
||||
// receivers have no queue to skip in (a queue would make them play things on
|
||||
// their own), so they ask us to do it when something else - google home, the
|
||||
// assistant, ... - requests a prev/next. same path as our own buttons.
|
||||
private fun listenToReceiver() {
|
||||
val session = castContext?.sessionManager?.currentCastSession ?: return
|
||||
try {
|
||||
session.setMessageReceivedCallbacks(OMNI_NAMESPACE) { _, _, message ->
|
||||
when (JSONObject(message).optString("action")) {
|
||||
"prev" -> runOnMainThread { eventMap.emitPrev() }
|
||||
"next" -> runOnMainThread { eventMap.emitNext() }
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
android.util.Log.w("OmniPlayer", "could not listen to the receiver", e)
|
||||
}
|
||||
}
|
||||
|
||||
val player: Player = runOnMainThreadSync {
|
||||
castOptions?.receiverApplicationId?.let { receiverApplicationId = it }
|
||||
@@ -124,7 +143,13 @@ class OmniPlayer(
|
||||
.setRemotePlayer(remote)
|
||||
.build()
|
||||
}
|
||||
active
|
||||
NavigationPlayer(
|
||||
active,
|
||||
hasPrev = { source?.metadata?.hasPrev == true },
|
||||
hasNext = { source?.metadata?.hasNext == true },
|
||||
onPrev = { eventMap.emitPrev() },
|
||||
onNext = { eventMap.emitNext() },
|
||||
)
|
||||
}
|
||||
|
||||
override var showNotification: Boolean? = false
|
||||
@@ -140,19 +165,19 @@ class OmniPlayer(
|
||||
when {
|
||||
shouldShow && !serviceRunning -> {
|
||||
val otherIsPlaying = notificationPlayer?.let { other ->
|
||||
other !== localPlayer && runOnMainThreadSync { other.isPlaying }
|
||||
other !== player && runOnMainThreadSync { other.isPlaying }
|
||||
} == true
|
||||
if (otherIsPlaying) {
|
||||
throw Error("Two players can't display notifications at the same time.")
|
||||
}
|
||||
notificationPlayer = localPlayer
|
||||
notificationPlayer = player
|
||||
ctx.startForegroundService(Intent(ctx, OmniPlayerService::class.java))
|
||||
serviceRunning = true
|
||||
}
|
||||
|
||||
!shouldShow && serviceRunning -> {
|
||||
ctx.stopService(Intent(ctx, OmniPlayerService::class.java))
|
||||
if (notificationPlayer == localPlayer) notificationPlayer = null
|
||||
if (notificationPlayer == player) notificationPlayer = null
|
||||
serviceRunning = false
|
||||
}
|
||||
}
|
||||
@@ -298,8 +323,8 @@ class OmniPlayer(
|
||||
}
|
||||
}
|
||||
|
||||
override val hasPrev: Boolean get() = player.hasPreviousMediaItem()
|
||||
override val hasNext: Boolean get() = player.hasNextMediaItem()
|
||||
override val hasPrev: Boolean get() = source?.metadata?.hasPrev == true
|
||||
override val hasNext: Boolean get() = source?.metadata?.hasNext == true
|
||||
override val status by mainThreadProperty {
|
||||
when (player.playbackState) {
|
||||
Player.STATE_IDLE,
|
||||
@@ -417,15 +442,10 @@ class OmniPlayer(
|
||||
value.castId,
|
||||
value.castData,
|
||||
)
|
||||
val mediaItems = mutableListOf<MediaItem>()
|
||||
if (value.metadata?.hasPrev == true) mediaItems.add(currentItem)
|
||||
mediaItems.add(currentItem)
|
||||
if (value.metadata?.hasNext == true) mediaItems.add(currentItem)
|
||||
|
||||
runOnMainThreadSync {
|
||||
val startIndex = if (value.metadata?.hasPrev == true) 1 else 0
|
||||
val startPositionMs = ((value.startTime ?: 0.0).coerceAtLeast(0.0)) * 1000.0
|
||||
player.setMediaItems(mediaItems, startIndex, startPositionMs.toLong())
|
||||
player.setMediaItems(listOf(currentItem), 0, startPositionMs.toLong())
|
||||
player.prepare()
|
||||
}
|
||||
syncNotificationService()
|
||||
@@ -447,11 +467,11 @@ class OmniPlayer(
|
||||
}
|
||||
|
||||
override fun playPrev() {
|
||||
runOnMainThreadSync { player.seekToPreviousMediaItem() }
|
||||
runOnMainThreadSync { eventMap.emitPrev() }
|
||||
}
|
||||
|
||||
override fun playNext() {
|
||||
runOnMainThreadSync { player.seekToNextMediaItem() }
|
||||
runOnMainThreadSync { eventMap.emitNext() }
|
||||
}
|
||||
|
||||
override fun selectVideo(video: Track) {
|
||||
@@ -481,15 +501,15 @@ class OmniPlayer(
|
||||
}?.mediaTrackGroup
|
||||
if (group != null) {
|
||||
params.setOverrideForType(TrackSelectionOverride(group, 0))
|
||||
}
|
||||
// also set preferred flags for cast selection
|
||||
val labels = track.label?.let { arrayOf(it) } ?: emptyArray()
|
||||
if (type == C.TRACK_TYPE_AUDIO) {
|
||||
params.setPreferredAudioLanguage(track.language)
|
||||
.setPreferredAudioLabels(*labels)
|
||||
} else {
|
||||
val labels = track.label?.let { arrayOf(it) } ?: emptyArray()
|
||||
if (type == C.TRACK_TYPE_AUDIO) {
|
||||
params.setPreferredAudioLanguage(track.language)
|
||||
.setPreferredAudioLabels(*labels)
|
||||
} else {
|
||||
params.setPreferredTextLanguage(track.language)
|
||||
.setPreferredTextLabels(*labels)
|
||||
}
|
||||
params.setPreferredTextLanguage(track.language)
|
||||
.setPreferredTextLabels(*labels)
|
||||
}
|
||||
}
|
||||
player.trackSelectionParameters = params.build()
|
||||
@@ -563,6 +583,10 @@ class OmniPlayer(
|
||||
const val CAST_ID_EXTRA = "dev.zoriya.omni.castId"
|
||||
const val CAST_DATA_EXTRA = "dev.zoriya.omni.castData"
|
||||
|
||||
// channel a receiver uses to ask us to play the prev/next media.
|
||||
// it must declare it too (`customNamespaces` in the caf options).
|
||||
const val OMNI_NAMESPACE = "urn:x-cast:dev.zoriya.omni"
|
||||
|
||||
// Receiver application id passed at runtime via OmniProvider's `cast`
|
||||
// prop; read by OmniCastOptionsProvider when the Cast SDK initializes.
|
||||
var receiverApplicationId: String? = null
|
||||
|
||||
@@ -244,6 +244,7 @@ class OmniView(val context: ThemedReactContext) :
|
||||
}
|
||||
|
||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||
if (isPlaying) scheduleVideoRebuild()
|
||||
updatePictureInPictureParams()
|
||||
}
|
||||
|
||||
@@ -377,22 +378,16 @@ class OmniView(val context: ThemedReactContext) :
|
||||
movedSurfaceToRootForPip = false
|
||||
}
|
||||
|
||||
// A surface that is destroyed and recreated while playback runs (the PIP
|
||||
// reparent) makes VLC drop its video pipeline; it won't rebuild it for the
|
||||
// new surface on its own, so we must ask it to. The rebuild only succeeds
|
||||
// once the (PIP) window has settled at its final size — firing it mid-resize
|
||||
// leaves the vout stopped — so it is debounced off surfaceChanged.
|
||||
private var pendingVideoRebuild = false
|
||||
private val rebuildRunnable = Runnable {
|
||||
if (!pendingVideoRebuild) return@Runnable
|
||||
pendingVideoRebuild = false
|
||||
boundPlayer?.rebuildVideoOutput()
|
||||
private val rebuildRunnable = Runnable { boundPlayer?.rebuildVideoOutput() }
|
||||
|
||||
private fun scheduleVideoRebuild() {
|
||||
surfaceView.removeCallbacks(rebuildRunnable)
|
||||
surfaceView.postDelayed(rebuildRunnable, 200)
|
||||
}
|
||||
|
||||
override fun surfaceCreated(holder: SurfaceHolder) {
|
||||
surfaceReady = true
|
||||
boundPlayer?.setVideoView(surfaceView)
|
||||
if (boundPlayer?.localPlayer?.isPlaying == true) pendingVideoRebuild = true
|
||||
updatePictureInPictureParams()
|
||||
}
|
||||
|
||||
@@ -403,10 +398,7 @@ class OmniView(val context: ThemedReactContext) :
|
||||
height: Int
|
||||
) {
|
||||
boundPlayer?.updateVideoLayout(width, height)
|
||||
if (pendingVideoRebuild) {
|
||||
surfaceView.removeCallbacks(rebuildRunnable)
|
||||
surfaceView.postDelayed(rebuildRunnable, 200)
|
||||
}
|
||||
scheduleVideoRebuild()
|
||||
}
|
||||
|
||||
override fun surfaceDestroyed(holder: SurfaceHolder) {
|
||||
|
||||
@@ -107,6 +107,7 @@ class VlcPlayer(ctx: Context) :
|
||||
@Volatile
|
||||
private var cachedBufferedPosition: Long = 0L
|
||||
private var boundSurfaceView: SurfaceView? = null
|
||||
private var videoOutputStale = false
|
||||
private var lastVideoSize: VideoSize = VideoSize.UNKNOWN
|
||||
|
||||
private val availableCommands: Player.Commands = Player.Commands.Builder()
|
||||
@@ -348,6 +349,7 @@ class VlcPlayer(ctx: Context) :
|
||||
playerError = null
|
||||
|
||||
player.stop()
|
||||
videoOutputStale = false
|
||||
|
||||
mediaItems.getOrNull(targetIndex)?.let { item ->
|
||||
val uri = item.localConfiguration?.uri?.toString()
|
||||
@@ -897,30 +899,22 @@ class VlcPlayer(ctx: Context) :
|
||||
if (surfaceView == null) return clearVideoSurface()
|
||||
if (vlcVout.areViewsAttached() && boundSurfaceView === surfaceView) return
|
||||
boundSurfaceView = surfaceView
|
||||
// Hand VLC the SurfaceView (not a raw Surface) so it reads the real view
|
||||
// size and keeps the video layout correct as the surface resizes (e.g. the
|
||||
// PIP window shrinking/growing); see updateVideoLayout for size updates.
|
||||
videoOutputStale = (
|
||||
player.playerState == IMedia.State.Playing ||
|
||||
player.playerState == IMedia.State.Paused
|
||||
)
|
||||
vlcVout.setVideoView(surfaceView)
|
||||
vlcVout.attachViews()
|
||||
}
|
||||
|
||||
/**
|
||||
* VLC tears down its video decoder + output whenever the output Surface is
|
||||
* destroyed (e.g. the SurfaceView being reparented for PIP) and does not
|
||||
* rebuild them when a new Surface arrives — the picture stays black. Toggling
|
||||
* the video track forces VLC to spin up a fresh decoder/vout against the
|
||||
* currently-attached Surface. Safe no-op when nothing is attached/playing.
|
||||
*/
|
||||
fun rebuildVideoOutput() {
|
||||
if (!vlcVout.areViewsAttached() || !player.isPlaying) return
|
||||
if (!videoOutputStale || !player.isPlaying || !vlcVout.areViewsAttached()) return
|
||||
videoOutputStale = false
|
||||
player.setVideoTrackEnabled(false)
|
||||
player.setVideoTrackEnabled(true)
|
||||
player.updateVideoSurfaces()
|
||||
}
|
||||
|
||||
/** Recompute the video layout for the current surface size. Must run whenever
|
||||
* the surface resizes (VLC latches a stale geometry otherwise -> the picture
|
||||
* renders at the wrong size, anchored in a corner). */
|
||||
fun updateVideoLayout(width: Int, height: Int) {
|
||||
if (!vlcVout.areViewsAttached()) return
|
||||
if (width > 0 && height > 0) vlcVout.setWindowSize(width, height)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
Reference in New Issue
Block a user