At my current work, we heavily rely on Android flavors. Unfortunately installing them with Android studio is problematic because of the scale of the project. Because of that we mostly use the gradle wrapper and the default tasks provided by gradle for building/installing the app.

My typical workflow was as follows:

  • run “./gradlew assemble{Variant}{BuildType}
  • find the path of the apk
  • run “adb install -r {PathToApk}”

I know there is an install{Variant}{BuildType} task, but I find it is not so reliable. There is an added problem that this task assembles the app first, so if you just want to install the apk you have to wait for the app to be rebuilt.

The variants in the project I’m working with, sometimes have more complex naming schemes which cause typos when running from the command line. The typos and the amount of manual steps motivated me to create a bash script which could make this process a lot more streamlined.

The first version of this script only installed the apk, it only needed the name or shortcut of the app to do that. After some feedback from the team I got a request to expand the functionality of the script. The current version can build, install and even open the app on the phone or emulator.

The app

For the purpose of this blog post I have made a basic app, which has flavors that somewhat reflect the variants in my work project. Here’s the companion project repository

The gradle variants are declared as following:

android {
    flavorDimensions "flavor", "api"
    productFlavors {
        apple {
            dimension "flavor"
            applicationIdSuffix ".apple"
        }
        orange {
            dimension "flavor"
            applicationIdSuffix ".orange"
        }
        appleCarrotLime {
            dimension "flavor"
            applicationIdSuffix ".appleCarrotLime"
        }
        mock {
            dimension "api"
            applicationIdSuffix ".mock"
        }
        production {
            dimension "api"
            applicationIdSuffix ".production"
        }
    }
}

Basically this app has 3 juice flavors all of which also have 2 variants depending on if the API should be production or mock, this gives a total of 6 flavors. Building the app from the command line using the default gradle tasks would look something like this:

$ ./gradlew assembleOrangeProductionDebug
$ ./gradlew assembleAppleMockDebug
$ ./gradlew assembleAppleCarrotLimeProductionDebug

After the build finishes an apk file is created. To install the app on the device, a path to the apk has to be provided:

$ adb install -r ./app/build/outputs/apk/appleCarrotLimeProduction/debug/app-appleCarrotLime-production-debug.apk

Overall this process is not complicated, but a little time-consuming. Fortunately the gradle tasks and build folders have the flavor names in them, which drastically simplified the script creation.

The script

I will break down the script to a list of steps and explain them, if you want the full code click here.

What the script can do:

- Show a help menu
- Install an app with the possibility to:
    - Build it before installing
    - Open it after the install

For this script to work with all the capabilities, adb and aapt have to be added to $PATH. The full script has a comment on the top with a little more information.

Getting the command line flags is achieved with getopts:

app=""
isMock=false
buildTheApp=false
openTheApp=false
showHelp=false

while getopts "a:mboh" flag; do
  case "${flag}" in
    a) app="${OPTARG}" ;;
    m) isMock=true ;;
    b) buildTheApp=true ;;
    o) openTheApp=true ;;
    h) showHelp=true ;;
  esac
done

Here are some examples:text

$ ./script.sh -h
sets the showHelp flag to true

$ ./script.sh -a apple -b
sets the app variable to "apple" and the buildTheApp flag to true

If the showHelp flag is set to true it simply echos some info, and stops the execution:

if [[ "$showHelp" == true ]]
then
    echo "Available flags:"
    echo "'-a app' what app to install"
    echo "'-m' use the mock version"
    echo "'-b' build the app"
    echo "'-o' open the app (only works if you add aapt to path)"
    exit 0
fi

Here are the flavor declarations, and a helper setAppFolderName function to check if the provided app/flavor name or shortcut is correct:

declare -a appMap=(
    "ap=apple"
    "or=orange"
    "acl=appleCarrotLime"
)

setAppFolderName(){
    #make the user app $1 input lowercase
    appNameArgument=$(echo "$1" | tr '[:upper:]' '[:lower:]')
    for i in "${!appMap[@]}"; do
      #split appMap element with "=" and assign them
      appShortcut=$(echo ${appMap[$i]} | cut -d'=' -f1)
      appFullName=$(echo ${appMap[$i]} | cut -d'=' -f2)
      if [[ "$appNameArgument" == "$appShortcut" ||
        "$appNameArgument" == "$appFullName" ]]
        then
            #set the return variable $2 (folderPrefix)
            eval "$2=$appFullName"
            break 1
      fi
    done
}

The way I return a variable from this script is with an eval assigment. $2 corresponds to the second variable passed in to this function. If the app name is correct, then the full name of the flavor is “returned” by assigning it to $2.

When the app is incorrect then setAppFolderName doesn’t return anything, when that happens the script asks for a correct name in a loop:

folderPrefix=""
setAppFolderName $app folderPrefix

while [ -z "$folderPrefix" ]; do
    for i in "${!appMap[@]}"; do
        echo ${appMap[$i]}
    done
    read -p "Enter a shortcut or full name: " input
    setAppFolderName $input folderPrefix
done

Setting the API flavor is done with a simple if assignment. By default, the Production flavor is chosen:

if [[ "$isMock" == true ]]
then
    type="Mock"
else
    type="Production"
fi

If the -b flag was present, then the app is built before the installation:

if [[ "$buildTheApp" == true ]]
then
    appNameCapitalized="$(tr '[:lower:]' '[:upper:]' <<< ${folderPrefix:0:1})${folderPrefix:1}"
    buildCommand="./gradlew assemble${appNameCapitalized}${type}Debug"
    echo "running: $buildCommand"
    eval "$buildCommand"
fi

Installing the app is a little more complicated, first the apk file has to exist before it is installed. If the apk exists, what happens if there are more than one apk file in the build folder? This shouldn’t happen, but if it does, then the apk with the most recent date is used:

appFolderPath="app/build/outputs/apk/$folderPrefix$type"
if [[ ! -d "$appFolderPath" ]]
then
    echo "$appFolderPath doesn't exist"
    exit 1
fi

#take the first apk from ls sorted with -t
appDebugFolder="$appFolderPath/debug"
fullFilePath=$(ls -t $appDebugFolder/*.apk | head -1)

#execute adb install -r on apk
echo "Installing $fullFilePath"
eval "adb install -r $fullFilePath"

Opening the app is as easy as copying the solution from stack overflow:

if [[ "$openTheApp" == true ]]
then
    pkg=$(aapt dump badging $fullFilePath|awk -F" " '/package/ {print $2}'|awk -F"'" '/name=/ {print $2}')
    act=$(aapt dump badging $fullFilePath|awk -F" " '/launchable-activity/ {print $2}'|awk -F"'" '/name=/ {print $2}')
    eval "adb shell am start -n $pkg/$act"
fi

The benefit of opening the app this way is that no activities have to be specified. Even if the flavors have a different entry point, this script always opens the app using the main launcher activity.

I have been using this script for a couple of weeks now, and it made working with flavor’s a lot easier.

Here’s the link to the Juice app repository, and the script.

The script in action:

Just building and installing the appleCarrotLime production flavor
Building, installing and then opening the orange production flavor
The same as before but with a mock flavor
And without specifying any flavor