All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.triplet.gradle.androidpublisher.internal.TrackManager.kt Maven / Gradle / Ivy

Go to download

Gradle Play Publisher is a plugin that allows you to upload your App Bundle or APK and other app details to the Google Play Store.

The newest version!
package com.github.triplet.gradle.androidpublisher.internal

import com.github.triplet.gradle.androidpublisher.ReleaseStatus
import com.google.api.services.androidpublisher.model.LocalizedText
import com.google.api.services.androidpublisher.model.Track
import com.google.api.services.androidpublisher.model.TrackRelease

internal interface TrackManager {
    fun findHighestTrack(): Track?

    fun getReleaseNotes(): Map>

    fun update(config: UpdateConfig)

    fun promote(config: PromoteConfig)

    data class BaseConfig(
            val releaseStatus: ReleaseStatus?,
            val userFraction: Double?,
            val updatePriority: Int?,
            val releaseNotes: Map?,
            val retainableArtifacts: List?,
            val releaseName: String?,
    )

    data class UpdateConfig(
            val trackName: String,
            val versionCodes: List,
            val didPreviousBuildSkipCommit: Boolean,
            val base: BaseConfig,
    )

    data class PromoteConfig(
            val promoteTrackName: String,
            val fromTrackName: String,
            val versionCode: Long?,
            val base: BaseConfig,
    )
}

internal class DefaultTrackManager(
        private val publisher: InternalPlayPublisher,
        private val editId: String,
) : TrackManager {
    override fun findHighestTrack(): Track? {
        return publisher.listTracks(editId).maxByOrNull {
            it.releases.orEmpty().flatMap { it.versionCodes.orEmpty() }.maxOrNull() ?: 0
        }
    }

    override fun getReleaseNotes(): Map> {
        val releaseNotes = mutableMapOf>()

        val tracks = publisher.listTracks(editId)
        for (track in tracks) {
            val notes = track.releases?.maxByOrNull {
                it.versionCodes?.maxOrNull() ?: Long.MIN_VALUE
            }?.releaseNotes.orEmpty()

            releaseNotes[track.track] = notes
        }

        return releaseNotes
    }

    override fun update(config: TrackManager.UpdateConfig) {
        val track = if (config.didPreviousBuildSkipCommit) {
            createTrackForSkippedCommit(config)
        } else if (config.base.releaseStatus.orDefault().isRollout()) {
            createTrackForRollout(config)
        } else {
            createDefaultTrack(config)
        }

        publisher.updateTrack(editId, track)
    }

    override fun promote(config: TrackManager.PromoteConfig) {
        val track = publisher.getTrack(editId, config.fromTrackName)
        check(track.releases.orEmpty().flatMap { it.versionCodes.orEmpty() }.isNotEmpty()) {
            "Track '${config.fromTrackName}' has no releases. Did you mean to run publish?"
        }

        // Update the track
        for (release in track.releases) {
            release.mergeChanges(config.versionCode?.let { listOf(it) }, config.base)
        }
        // Only keep the unique statuses from the highest version code since duplicate statuses are
        // not allowed. This is how we deal with an update from inProgress -> completed. We update
        // all the tracks to completed, then get rid of the one that used to be inProgress.
        track.releases = track.releases.sortedByDescending {
            it.versionCodes?.maxOrNull()
        }.distinctBy {
            it.status
        }

        println("Promoting release from track '${track.track}'")
        track.track = config.promoteTrackName
        publisher.updateTrack(editId, track)
    }

    private fun createTrackForSkippedCommit(config: TrackManager.UpdateConfig): Track {
        val track = publisher.getTrack(editId, config.trackName)

        if (track.releases.isNullOrEmpty()) {
            track.releases = listOf(TrackRelease().mergeChanges(config.versionCodes, config.base))
        } else {
            val hasReleaseToBeUpdated = track.releases.firstOrNull {
                it.status == config.base.releaseStatus.orDefault().publishedName
            } != null

            if (hasReleaseToBeUpdated) {
                for (release in track.releases) {
                    if (release.status == config.base.releaseStatus.orDefault().publishedName) {
                        release.mergeChanges(
                                release.versionCodes.orEmpty() + config.versionCodes, config.base)
                    }
                }
            } else {
                val release = TrackRelease().mergeChanges(config.versionCodes, config.base).apply {
                    maybeCopyChangelogFromPreviousRelease(config.trackName)
                }
                track.releases = track.releases + release
            }
        }

        return track
    }

    private fun createTrackForRollout(config: TrackManager.UpdateConfig): Track {
        val track = publisher.getTrack(editId, config.trackName)

        val keep = track.releases.orEmpty().filterNot { it.isRollout() }
        val release = TrackRelease().mergeChanges(config.versionCodes, config.base).apply {
            maybeCopyChangelogFromPreviousRelease(config.trackName)
        }
        track.releases = keep + release

        return track
    }

    private fun createDefaultTrack(config: TrackManager.UpdateConfig) = Track().apply {
        track = config.trackName
        val release = TrackRelease().mergeChanges(config.versionCodes, config.base).apply {
            maybeCopyChangelogFromPreviousRelease(config.trackName)
        }
        releases = listOf(release)
    }

    private fun TrackRelease.maybeCopyChangelogFromPreviousRelease(trackName: String) {
        if (!releaseNotes.isNullOrEmpty()) return

        val previousRelease = publisher.getTrack(editId, trackName)
                .releases.orEmpty()
                .maxByOrNull { it.versionCodes.orEmpty().maxOrNull() ?: 1 }
        releaseNotes = previousRelease?.releaseNotes
    }

    private fun TrackRelease.mergeChanges(
            versionCodes: List?,
            config: TrackManager.BaseConfig,
    ) = apply {
        updateVersionCodes(versionCodes, config.retainableArtifacts)
        updateStatus(config.releaseStatus, config.userFraction != null)
        updateConsoleName(config.releaseName)
        updateReleaseNotes(config.releaseNotes)
        updateUserFraction(config.userFraction)
        updateUpdatePriority(config.updatePriority)
    }

    private fun TrackRelease.updateVersionCodes(versionCodes: List?, retainableArtifacts: List?) {
        val newVersions = versionCodes ?: this.versionCodes.orEmpty()
        this.versionCodes = newVersions + retainableArtifacts.orEmpty()
    }

    private fun TrackRelease.updateStatus(releaseStatus: ReleaseStatus?, hasUserFraction: Boolean) {
        if (releaseStatus != null) {
            status = releaseStatus.publishedName
        } else if (hasUserFraction) {
            status = ReleaseStatus.IN_PROGRESS.publishedName
        } else if (status == null) {
            status = DEFAULT_RELEASE_STATUS.publishedName
        }
    }

    private fun TrackRelease.updateConsoleName(releaseName: String?) {
        if (releaseName != null) name = releaseName
    }

    private fun TrackRelease.updateReleaseNotes(rawReleaseNotes: Map?) {
        val releaseNotes = rawReleaseNotes.orEmpty().map { (locale, notes) ->
            LocalizedText().apply {
                language = locale
                text = notes
            }
        }
        val existingReleaseNotes = this.releaseNotes.orEmpty()

        this.releaseNotes = if (existingReleaseNotes.isEmpty()) {
            releaseNotes
        } else {
            val merged = releaseNotes.toMutableList()

            for (existing in existingReleaseNotes) {
                if (merged.none { it.language == existing.language }) merged += existing
            }

            merged
        }
    }

    private fun TrackRelease.updateUserFraction(userFraction: Double?) {
        if (userFraction != null) {
            this.userFraction = userFraction.takeIf { isRollout() }
        } else if (isRollout() && this.userFraction == null) {
            this.userFraction = DEFAULT_USER_FRACTION
        } else if (!isRollout()) {
            this.userFraction = null
        }
    }

    private fun TrackRelease.updateUpdatePriority(updatePriority: Int?) {
        if (updatePriority != null) {
            inAppUpdatePriority = updatePriority
        }
    }

    private fun ReleaseStatus.isRollout() =
            this == ReleaseStatus.IN_PROGRESS || this == ReleaseStatus.HALTED

    private fun TrackRelease.isRollout() =
            status == ReleaseStatus.IN_PROGRESS.publishedName ||
                    status == ReleaseStatus.HALTED.publishedName

    private fun ReleaseStatus?.orDefault() = this ?: DEFAULT_RELEASE_STATUS

    private companion object {
        const val DEFAULT_USER_FRACTION = 0.1
        val DEFAULT_RELEASE_STATUS = ReleaseStatus.COMPLETED
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy