Skip to main content

Gradle: Configuring assets folder of Android application

  In many projects I've worked I've come to appreciate the advantages that using a continuous integration tool like TeamCity. During the development of  last Android application, available on Google Play at OCL Lab Results, I saw the need to include or exclude certain files in the assets of the same.

   I decided to go into the benefits offered by a build automation system like Gradle. Let me make one thing clear before starting the details, this article is not a tutorial on Gradle, so I hope you're familiar with it before reading, although I confess that it is not very difficult to understand its main components. All code shown below is designed with Gradle 2.1 and Android Studio.

  First you need to ensure the existence of the folder of assets to be included in the apk. So we will create the next task to verify the existence of that folder:

def assetsProjectFolderPath= '/src/main/assets'
 
task createAssetsFolder <<{
    def folder = file("$assetsProjectFolderPath")
    if(!folder.exists()){
        folder.mkdirs()
    }

    fileTree (dir: "$assetsProjectFolderPath").visit { def asset ->
        asset.file.delete()
    }
}
The createAssetsFolder task, guarantees the existence of the assets folder, and delete all the files it contains before starting the copy. To verify that above code works correctly, we can invoke the task from the command line in Terminal:

gradle createAssetsFolder
 Now we can create a folder for each of the stages of development that we want to use and copy each asset you want to include in our .apk file. To distinguish the included assets in each development environments we will use the variable env, that correspond to each of the previously created folders. For example, in the project I created a folder called assets, which has three folders, one for each of the stages of development : debug, staging and release.



We can begin to copy each asset file to the assets folder of our future apk , let´s call this task  copyAssetFiles, note that the value of the assetsSourceFolderPath variable is the relative path of the assets folder to build.gradle file of your project:
def assetsSourceFolderPath     = 'assets'
 
task copyAssetFiles(dependsOn: createAssetsFolder) << {
    fileTree( dir: "$assetsSourceFolderPath/$env").each{ asset ->
        copy{
            from asset
            into "$assetsProjectFolderPath"
        }
    }
}
    You can watch as stated dependence between copyAssetFiles and createAssetsFolder tasks. To run the copyAssetFiles task you need to specify the env parameter, like on the following command:

gradle copyAssetFiles -Penv=release

   We are now ready to generate our .apk file with the assets already included, so we write the following task:

task assembleArtifact(dependsOn:[ clean,copyAssetFiles,assembleRelease ]) << {
    println "Assembled artifact for $env environment."
}

This is not enough, since the tasks of which depends assembleArtifact be executed before the same but in any order. Therefore, it is possible that the assembleRelease task runs without is executed before the copyAssetFiles task, so we need to specify the order of precedence of them.
 
copyAssetFiles.mustRunAfter clean
assembleRelease.mustRunAfter copyAssetFiles

 We are ready to configure our TeamCity. On Gradle step options, we have a Gradle Parameters section, you need specify on Gradle tasks option a task to invoke, in this case:
 
assembleArtifact -Penv=staging
 or
assembleArtifact -Penv=release

    We have achieved the main thing: The code works !!! But is not everything, we need to go beyond. The code does not look good, because we have two hard coded variables. Let's ask ourselves a question first. How Gradle knows wich is our assets folder for inclusion in the apk? If you open the file with extension *.iml, get the answer:

So let's parse this file with XmlPullParser to obtain the variable that we need. Let's take our code to another level, using one of the benefits of Groovy, which allows us to define our tasks as POGOs, YESS!!!!, as you readed, Plain Old Groovy Objects. When we write a code must try to make it more flexible and reusable as possible. Therefore we will implement a method to extract the value of an attribute will in an xml file.

class FindXmlElementAttributeTask extends DefaultTask{

    @Input

    def inputFile, charset, elementName, attributeName, filters

    @TaskAction

    def action(){
        ext.attributeValue = null
 
        //...Parse xml file and set ext.attributeValue
        
    }

}
   This is unlike what we saw!!!! We will explain in broad terms the main elements. This class extends DefaultTask class that represents the basis of the tasks that we had seen before. The @Input annotation indicates that these values ​​are parameters of the class. The inputFile parameter represents the file to parse, charset is the name of the encoding of the file, elementName is the name of the element we're looking for, attributeName is the name of the attribute will find and filters is a map with matches between key and value of the item to be searched. The @TaskAction indicates the method to execute when the task is invoked. The expression ext.attributeValue it is merely a property of the task.

You can set the task to find the path of assets folder as:

task findAssetsFolderPath(type: FindXmlElementAttributeTask){
    inputFile file("$project.name" + ".iml")
    charset 'utf-8'
    elementName 'option'
    attributeName 'value'
    filters = [ name : 'ASSETS_FOLDER_RELATIVE_PATH']
}

The expression $project.name return the name of your project. If we want the path of AndroidManifest.xml file, only have to declare the task as follow:

task findManifestPath(type: FindXmlElementAttributeTask){
    inputFile file("$project.name" + ".iml")
    charset 'utf-8'
    elementName 'option'
    attributeName 'value'
    filters = [ name : 'MANIFEST_FILE_RELATIVE_PATH']
}
 
Let's redefine the copyAssetFiles task:

task copyAssetFiles(dependsOn: findAssetsFolderPath) << {

    def folder = file(findAssetsFolderPath.attributeValue)
    if(!folder.exists()){
        folder.mkdirs()
    }

    fileTree (dir: findAssetsFolderPath.attributeValue)
        .visit { def asset -> asset.file.delete() }

    fileTree( dir: "assets/$env").each{ asset ->
        copy{
            from asset
            into findAssetsFolderPath.attributeValue
        }
    }
}

Now we are ready, in the Gradle Settings of your Continuous Integration, invoke one of the following tasks:
  • assembleArtifact -Penv=release    (To deploy with production assets)
  • assembleArtifact -Penv=staging   (To deploy with staging assets)

Download source code here.

 

Comments

Popular posts from this blog

Generate self signed certificate with OpenSSL for IIS

Recently I wanted to enable SSL to a project hosted on IIS 8. Finally the tool I used was   OpenSSL , after many days fighting with   makecert   commands.The certificate is generated in Debian, but I could import it seamlessly into IIS 7 and 8. Download the  OpenSSL  compatible with your OS and setup the configuration file. Set the configuration file as default configuration of OpenSSL. # OpenSSL configuration file. # # Establish working directory. dir = . [ ca ] default_ca = CA_default [ CA_default ] serial = $dir/serial database = $dir/certindex.txt new_certs_dir = $dir/certs certificate = $dir/cacert.pem private_key = $dir/private/cakey.pem default_days = 365 default_md = md5 preserve = no email_in_dn = no nameopt = default_ca certopt = default_ca policy = policy_match [ policy_match ] countryName = match stateOrProvinceName

Configure SSL on MS SQL Server with OpenSSL

I configured successfully SSL on Microsoft SQL Server 2012 Express Edition for the purpose of encrypting external network connections to the database that are made through Internet. For performance reasons for internal clients on the network I do not want to force the use of SSL and leave to the clients the option of use it or not. I set   Force Encryption   to   No   with the following steps: Sql Server Configuration Manager Sql Server Network Configuration Protocols for (MYSQLSERVERNAME) Right click:  Properties Flags  tab. When I try to establish an encrypted connection with Microsoft Sql Server Management Studio checking  Encrypt connection  option on  Options  >  Connection Properties  I get the following error. A connection was successfully established with the server, but then an error occurred during the login process. (provider: SSL Provider, error: 0 - The target principal name is incorrect.) (Microsoft SQL Server, Error: -2146893022) What is striking is that if I select 

Docker multi-stage build for Spring Boot application

  The post of this blog is to show the advantages of using multi-stage build on Docker for image size optimization. In case you've gotten here and never heard of the word "multi-stage", don't worry. Basically it is to build your image in different stages, and copy the artifacts from one stage to another, this allows you to keep the readability and maintenance of your Dockerfile, as well as reduce the size of the resulting image. For practice, let's use this Spring Boot learning project here , in which we use Maven to compile our project. Then let's not wait any longer, and let's get to work. Variant 1 This was the first variant that crossed my mind, basically it was, let me use a JDK base image to be able to compile the project, in this case openjdk:8, install Maven, compile the source code, and declare the entrypoint to the artifact binary .jar file. FROM openjdk:8 RUN apt-get update && apt-get install -y maven COPY . /usr/app WORKDIR