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

com.github.triplet.gradle.play.tasks.PublishListings.kt Maven / Gradle / Ivy

package com.github.triplet.gradle.play.tasks

import com.android.build.gradle.api.ApplicationVariant
import com.github.triplet.gradle.common.utils.orNull
import com.github.triplet.gradle.common.utils.readProcessed
import com.github.triplet.gradle.play.PlayPublisherExtension
import com.github.triplet.gradle.play.internal.AppDetail
import com.github.triplet.gradle.play.internal.GRAPHICS_PATH
import com.github.triplet.gradle.play.internal.ImageType
import com.github.triplet.gradle.play.internal.LISTINGS_PATH
import com.github.triplet.gradle.play.internal.ListingDetail
import com.github.triplet.gradle.play.tasks.internal.PublishEditTaskBase
import com.github.triplet.gradle.play.tasks.internal.WriteTrackExtensionOptions
import com.github.triplet.gradle.play.tasks.internal.workers.EditWorkerBase
import com.github.triplet.gradle.play.tasks.internal.workers.copy
import com.github.triplet.gradle.play.tasks.internal.workers.paramsForBase
import com.google.common.hash.Hashing
import com.google.common.io.Files
import org.gradle.api.file.Directory
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.FileCollection
import org.gradle.api.file.FileType
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.SkipWhenEmpty
import org.gradle.api.tasks.TaskAction
import org.gradle.kotlin.dsl.submit
import org.gradle.kotlin.dsl.support.serviceOf
import org.gradle.work.Incremental
import org.gradle.work.InputChanges
import org.gradle.workers.WorkerExecutor
import java.io.File
import java.io.Serializable
import javax.inject.Inject

internal abstract class PublishListings @Inject constructor(
        extension: PlayPublisherExtension,
        variant: ApplicationVariant
) : PublishEditTaskBase(extension, variant), WriteTrackExtensionOptions {
    @get:Internal
    internal abstract val resDir: DirectoryProperty

    @get:Incremental
    @get:PathSensitive(PathSensitivity.RELATIVE)
    @get:InputFiles
    protected val detailFiles: FileCollection by lazy {
        resDir.asFileTree.matching {
            // We can't simply use `project.files` because Gradle would expect those to exist for
            // stuff like `@SkipWhenEmpty` to work.
            for (detail in AppDetail.values()) include("/${detail.fileName}")
        }
    }

    @get:Incremental
    @get:PathSensitive(PathSensitivity.RELATIVE)
    @get:InputFiles
    protected val listingFiles: FileCollection by lazy {
        resDir.asFileTree.matching {
            for (detail in ListingDetail.values()) include("/$LISTINGS_PATH/*/${detail.fileName}")
        }
    }

    @get:Incremental
    @get:PathSensitive(PathSensitivity.RELATIVE)
    @get:InputFiles
    protected val mediaFiles: FileCollection by lazy {
        resDir.asFileTree.matching {
            for (image in ImageType.values()) {
                include("/$LISTINGS_PATH/*/$GRAPHICS_PATH/${image.dirName}/*")
            }
        }
    }

    // Used by Gradle to skip the task if all inputs are empty
    @Suppress("MemberVisibilityCanBePrivate", "unused")
    @get:PathSensitive(PathSensitivity.RELATIVE)
    @get:SkipWhenEmpty
    @get:InputFiles
    protected val targetFiles: FileCollection by lazy { detailFiles + listingFiles + mediaFiles }

    // This directory isn't used, but it's needed for up-to-date checks to work
    @Suppress("MemberVisibilityCanBePrivate", "unused")
    @get:Optional
    @get:OutputDirectory
    protected val outputDir = null

    @TaskAction
    fun publishListing(changes: InputChanges) {
        val details = processDetails(changes)
        val listings = processListings(changes)
        val media = processMedia(changes)

        if (details == null && listings.isEmpty() && media.isEmpty()) return

        project.serviceOf().noIsolation().submit(Publisher::class) {
            paramsForBase(this)

            this.details.set(details)
            this.listings.set(listings)
            this.media.set(media)
        }
    }

    private fun processDetails(changes: InputChanges): Directory? {
        val changedDetails =
                changes.getFileChanges(detailFiles).filter { it.fileType == FileType.FILE }
        if (changedDetails.isEmpty()) return null
        if (AppDetail.values().map { resDir.file(it.fileName) }.none { it.get().asFile.exists() }) {
            return null
        }

        return resDir.get()
    }

    private fun processListings(
            changes: InputChanges
    ) = changes.getFileChanges(listingFiles).asSequence()
            .filter { it.fileType == FileType.FILE }
            .map { it.file.parentFile }
            .distinct()
            // We can't filter out FileType#REMOVED b/c otherwise we won't publish the changes
            .filter { it.exists() }
            .toList()

    private fun processMedia(
            changes: InputChanges
    ) = changes.getFileChanges(mediaFiles).asSequence()
            .filter { it.fileType == FileType.FILE }
            .map { it.file.parentFile }
            .map { f -> f to ImageType.values().find { f.name == it.dirName } }
            .filter { it.second != null }
            .filter { it.first.exists() }
            .map { it.first to it.second!! }
            .associate { it }
            .map { Publisher.Media(it.key, it.value) }

    internal abstract class Publisher @Inject constructor(
            private val executor: WorkerExecutor
    ) : EditWorkerBase() {
        override fun execute() {
            if (parameters.details.isPresent) {
                executor.noIsolation().submit(DetailsUploader::class) {
                    parameters.copy(this)
                    dir.set(parameters.details)
                }
            }
            for (listing in parameters.listings.get()) {
                executor.noIsolation().submit(ListingUploader::class) {
                    parameters.copy(this)
                    listingDir.set(listing)
                }
            }
            for (medium in parameters.media.get()) {
                executor.noIsolation().submit(MediaUploader::class) {
                    parameters.copy(this)

                    imageDir.set(medium.imageDir)
                    imageType.set(medium.type)
                }
            }

            executor.await()
            commit()
        }

        interface Params : EditPublishingParams {
            val details: DirectoryProperty // Optional
            val listings: ListProperty
            val media: ListProperty
        }

        data class Media(val imageDir: File, val type: ImageType) : Serializable
    }

    internal abstract class DetailsUploader : EditWorkerBase() {
        override fun execute() {
            val defaultLanguage = AppDetail.DEFAULT_LANGUAGE.read()
            val contactEmail = AppDetail.CONTACT_EMAIL.read()
            val contactPhone = AppDetail.CONTACT_PHONE.read()
            val contactWebsite = AppDetail.CONTACT_WEBSITE.read()

            println("Uploading app details")
            edits.publishAppDetails(defaultLanguage, contactEmail, contactPhone, contactWebsite)
        }

        private fun AppDetail.read() =
                parameters.dir.get().file(fileName).asFile.orNull()?.readProcessed()

        interface Params : EditPublishingParams {
            val dir: DirectoryProperty
        }
    }

    internal abstract class ListingUploader : EditWorkerBase() {
        override fun execute() {
            val locale = parameters.listingDir.get().asFile.name
            val title = ListingDetail.TITLE.read()
            val shortDescription = ListingDetail.SHORT_DESCRIPTION.read()
            val fullDescription = ListingDetail.FULL_DESCRIPTION.read()
            val video = ListingDetail.VIDEO.read()
            if (title == null &&
                    shortDescription == null &&
                    fullDescription == null &&
                    video == null) {
                return
            }

            println("Uploading $locale listing")
            edits.publishListing(locale, title, shortDescription, fullDescription, video)
        }

        private fun ListingDetail.read() =
                parameters.listingDir.get().file(fileName).asFile.orNull()?.readProcessed()

        interface Params : EditPublishingParams {
            val listingDir: DirectoryProperty
        }
    }

    internal abstract class MediaUploader : EditWorkerBase() {
        override fun execute() {
            val typeName = parameters.imageType.get().publishedName
            val files = parameters.imageDir.asFileTree.sorted()
                    .filterNot { it.extension == "index" }
            check(files.size <= parameters.imageType.get().maxNum) {
                "You can only upload ${parameters.imageType.get().maxNum} $typeName."
            }

            val locale = parameters.imageDir.get().asFile // icon
                    .parentFile // graphics
                    .parentFile // en-US
                    .name
            val remoteHashes = edits.getImages(locale, typeName).map { it.sha256 }
            val localHashes = files.map {
                Files.asByteSource(it).hash(Hashing.sha256()).toString()
            }
            if (remoteHashes == localHashes) return

            edits.publishImages(locale, typeName, files)
        }

        interface Params : EditPublishingParams {
            val imageDir: DirectoryProperty
            val imageType: Property
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy