BLOG
19 September 2019

Continuous Integration for Xamarin Android Project in TeamCity

tech

When first approaching Continuous Integration for Xamarin Android Project in TeamCity, it took me a few hours and some nerves to get it done. Here’s a step-by-step tutorial for you to spare you some troubles.

It is not an easy job to create a project that works the same way locally and on any Continuous Integration tool. Team City is a great tool, but it doesn’t have built-in plugins for Xamarin Android so we will have to move locally tested commands to build steps.

In this article, I will show you how to:

  • Use appsettings.json and AndroidManifest.xml based on Configuration.
  • Create apk files from Visual Studio.
  • Create an apk from the command line.
  • Set up a TeamCity for your project.

For this example, I made two assumptions:

  • You have basic knowledge of developing Xamarin Android apps in Visual Studio.
  • You have your TeamCity already up and running.

Configurations

We will need more than one Configuration to determine which appsettings.json and AndroidManifest.xml to use.

Firstly, go to Configuration Manager and create 3 new configurations: DEV, STAGE, PROD.

Remember: copy settings from Release as debugging is turned off there.

configuration
      manager

Reading data from AndroidManifest.xml

This part is the easiest. All you need is to go to content_main.axml and create a new TextView.

1 2 3 <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/textview_appName"/>

Go to MainActivity.cs and add this to OnCreate method:

1 2 3 TextView textview_appName = FindViewById<TextView>(Resource.Id.textview_appName); textview_appName.Text = $"PackageName: {PackageName.Split('.').ToList().Last()}";

After running an application onscreen you will have the same value as you have in the AndroidManifest.xml > manifest tag > package attribute.

XamarinTestApp

Multiple AndroidManifest.xml files

You may ask – why do I need to use a different AndroidManifest.xml?

The answer is easy – if you want to deploy your DEV, STAGE and PROD version of your app to Google Play, each of them must have a different name. The name of an app is stored in AndroidManifest.xml so you must have one of them for each app.

Go to the Properties folder (your AndroidManifest.xml is already there) and create 3 additional folders: DEV, STAGE, PROD and copy your current AndroidManifest.xml to each of those.

Edit each file and change manifest tag > package attribute in DEV, STAGE and PROD folder to:

  • com.companyname.XamarinTestApp_DEV
  • com.companyname.XamarinTestApp_ STAGE
  • com.companyname.XamarinTestApp_ PROD

Now unload your project and edit .csproj file and add <AndroidManifest></AndroidManifest> to each PropertyGroup with the correct path to your AndfoidManifest files. It should look like the one below.

1 2 3 4 5 6 7 8 <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'DEV|AnyCPU'"> ... <AndroidManifest>Properties\DEV\AndroidManifest.xml</AndroidManifest> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'STAGE|AnyCPU'"> ... <AndroidManifest>Properties\STAGE\AndroidManifest.xml</AndroidManifest> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'PROD|AnyCPU'"> ... <AndroidManifest>Properties\PROD\AndroidManifest.xml</AndroidManifest> </PropertyGroup>

Now change your configuration to DEV, STAGE and PROD, run an app and check the app name on-screen.

appname dev
appname stage
appname prod

Reading data from appsetting.json

If you have your way of reading data from this file, that is fine. However, for those who don’t, I will show you a piece of code with some minimal explanation.

1. Go to the Assets folder and create an appsettings.json file with this content:

{ "Version": "Debug" }

2. Create an IAppSettings interface:

1 2 public interface IAppSettings { string Version { get; set; } }

And AppSettings class:

1 2 public class AppSettings : IAppSettings { public string Version { get; set; } }

3. Create IAppSettingsManager interface:

1 2 public interface IAppSettingsManager { IAppSettings GetConfig(); }

And AppSettingsManager class:

1 2 3 4 5 6 using Android.Content.Res; using Newtonsoft.Json; using System.IO; public class AppSettingsManager : IAppSettingsManager { const string _appSettingsFileName = "appsettings.json"; private readonly AssetManager _manager; public AppSettingsManager(AssetManager manager) { _manager = manager; } public IAppSettings GetConfig() { using (var sr = new StreamReader(_manager.Open(_appSettingsFileName))) { var content = sr.ReadToEnd(); var configuration = JsonConvert.DeserializeObject<AppSettings>(content); return configuration; } } }

I’m using Autofac for my dependency injection so my App.cs file looks like this:

1 2 3 4 5 6 7 8 9 using Android.App; using Android.Runtime; using Autofac; using Autofac.Extras.CommonServiceLocator; using CommonServiceLocator; using System; [Application] public class App : Application { public App(IntPtr handle, JniHandleOwnership transfer) : base(handle, transfer) { } public override void OnCreate() { var containerBuilder = new ContainerBuilder(); ConfigureContainer(containerBuilder); var container = containerBuilder.Build(); ServiceLocator.SetLocatorProvider(() => new AutofacServiceLocator(container)); base.OnCreate(); } private void ConfigureContainer(ContainerBuilder containerBuilder) { containerBuilder.Register(c => new AppSettingsManager(this.Assets)) .As<IAppSettingsManager>(); containerBuilder.Register(c => { var mgr = c.Resolve<IAppSettingsManager>(); return mgr.GetConfig(); }).As<IAppSettings>(); } }

I have registered a set of AppSettingsManager and AppSettings.

5. Next is the last DependencyResolver class:

1 2 3 4 using Autofac; using Autofac.Extras.CommonServiceLocator; using CommonServiceLocator; public static class DependencyResolver { public static T Get<T>() { var serviceLocator = (AutofacServiceLocator)ServiceLocator.Current; var ctx = serviceLocator.GetInstance<IComponentContext>(); return ctx.Resolve<T>(); } }

6. Now we are ready to read data from the appsetting.json file, so add the new TextView to your content_main.axml below the textview_appSettings:

1 2 3 <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/textview_appSettings"/>

Be sure to add this in the MainActivity in OnCreate method.

1 2 3 4 TextView textview_appSettings = FindViewById<TextView>(Resource.Id.textview_appSettings); var version = DependencyResolver.Get<IAppSettings>().Version; textview_appSettings.Text = $"AppSettings: {version}";

Now, when you run your app you should see an additional line such as this on your screen.

XamarinTestApp
      Debug

Multiple appsetting.json files

Go to the Assets folder and create 4 new folders (Debug, DEV, STAGE, PROD) and copy the appsetting.json to each of them. Change the value of the Version to match the folder name. Unload the project and edit the csproj file as we have to add a Copy task that will be triggered not only before the build but just after all the files are copied to the build directory.

If you think that this will solve your problem, then you’re mistaken. This will happen before the build, but after all files are copied to a build directory, so it will work for the second build, which in-effect is pointless.

1 2 3 4 <Target Name="CopyConfigFiles" BeforeTargets="Build"> <Delete Files="$(MSBuildProjectDirectory)/Assets/appsettings.json" /> <Copy SourceFiles="$(MSBuildProjectDirectory)/Assets/$(Configuration)/appsettings.json" DestinationFolder="$(MSBuildProjectDirectory)/Assets/" /> </Target>
msbuild

Everything is already copied to the \obj\${Configuration}\90 directory before our CopyConfigFiles task is triggered so we have to add a new PropertyGroup that should look like the one below:

1 2 3 4 5 6 <PropertyGroup> <PrepareForRunDependsOn>$(PrepareForRunDependsOn);CopyConfigFiles</PrepareForRunDependsOn> </PropertyGroup> <ProjectExtensions /> <Target Name="CopyConfigFiles"> <Delete Files="$(MSBuildProjectDirectory)/Assets/appsettings.json" /> <Copy SourceFiles="$(MSBuildProjectDirectory)/Assets/$(Configuration)/appsettings.json" DestinationFolder="$(MSBuildProjectDirectory)/Assets/" /> </Target>
msbuild-2

Now, it is working as expected. Change your Configuration and run the apps. You should now see values from the correct appsetting.json.

Great! Our solution is working, so now we have to create a package from Visual Studio and check if it is still working.

Creating a package from Visual Studio

To create a package from Visual Studio and run it on the device, open the properties of your project, go to Android Options and unselect “Use Shared runtime” for DEV, STAGE and PROD configuration.

Next, right-click on the project and select Archive. You should see the Distribute button on the right; select it. After selecting Ad Hoc, create a key (remember the password, because you will be asked for it).

Now, save it and click Open Folder.

Copy the apk from the signed-aps folder and paste it to your device. Install the apk on your device and check if everything is working as before.

Creating a package from the command line

Now, to create a package from the command line and run on the device, go to Android Options > Debugging Options. UNCHECK the Enable Developer Instrumentation box.

Before you run MSBuild from the command line, you have to unload the project. Edit the csproj file and paste this to the bottom, because you will not receive an apk file straight after building.

Open the command line, and run MSBuild with the Configuration parameters and PackageAndroidApk_DEV targets (you may have a different path to MSBuild).

1 2 3 "c:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\MSBuild.exe" XamarinTestApp.csproj /p:Configuration=DEV /t:PackageAndroidApk_DEV

Your apk file should appear in the \bin\DEV\ folder.

Signing an app

Now we have to sign this file, so we have to find the keys we created some time ago.

  1. Go to C:\Users{YourUserName}\AppData\Local\Xamarin\Mono for Android\Keystore and find a keystore you have created.
  2. Copy both files to your project directory.
  3. Use jarsigner to sign an app (try to find jarsigner.exe on your disk, you may have a different location).
1 2 3 4 5 "c:\Program Files\Android\jdk\microsoft_dist_openjdk_1.8.0.25\bin\jarsigner.exe" -verbose -sigalg md5withRSA -digestalg SHA1 -keystore xamarin.keystore -storepass xamarin -keypass xamarin -signedjar bin\DEV\com.companyname.XamarinTestApp_DEV-signed.apk bin\DEV\com.companyname.XamarinTestApp_DEV.apk xamarin
  • Keystore – the name of your file
  • Storepass – your chosen password
  • Keypass – the same password
  • Signedjar – path to the apk signed file
  • Verbose – because your certificate is self-signed

A new file com.companyname.XamarinTestApp_DEV-signed.apk should appear in the same folder.

ZIPAligning

The last step is to ZIPAligne this file (Google Play requires this). To do so use this command (try to find zipalign.exe on your disk, you may have a different location).

1 2 3 4 "c:\Program Files (x86)\Android\android-sdk\build-tools\28.0.3\zipalign.exe" -f -v 4 bin\DEV\com.companyname.XamarinTestApp_DEV-signed.apk bin\DEV\com.companyname.XamarinTestApp_DEV-zipaligned.apk

Now, the final file is available to distribute to your device. Please check if this one is working as the previous one.

TeamCity

Finally, we can go to the last step – TeamCity. Create a new project, connect to your GitHub and edit the configuration settings.

Parameters

As you may notice when running MSBuild, there is a step _ResolveMonoAndroidSdks

Use your own Java SDK path in the JavaSdkPath parameter (MSBuild used by TeamCity is not smart enough to find the JavaSdkDirectory, and without specifying it as a system property you will receive an error) and use the same ZipAlignPath that you have used before.

Go to the Parameters section and add the following:

  • Configuration Parameters
  • System Properties

Build Steps

Go to build steps, and commence the 5 step process:

1. Nuget Install

2. Clean Solution

3. Package Android (APK)

4. Sign Android Package

Custom script:

1 2 3 4 "%JarSignerPath%" -verbose -sigalg md5withRSA -digestalg SHA1 -keystore xamarin.keystore -storepass xamarin -keypass xamarin -signedjar bin\%Version%\com.companyname.%PackageName%-signed.apk bin\%Version%\com.companyname.%PackageName%.apk xamarin

5. Zip Align Android Package

Custom script:

1 2 3 "%ZipAlignPath%" -f -v 4 bin\%Version%\com.companyname.%PackageName%-signed.apk bin\%Version%\com.companyname.%PackageName%-zipaligned.apk

Now click ‘RUN’. After the successful build, go to general settings to specify Artifact paths - bin\DEV => bin\DEV and select RUN again.

Now you should see artifacts after a build.

If you want to do the same for STAGE and PROD just:

  1. Return to editing your configurations and select Copy configuration from the Action dropdown
  2. Change the value of the Version parameter to STAGE or PROD instead of DEV
  3. Change Artifact paths to bin\STAGE=> bin\STAGE or bin\PROD=> bin\PROD.

That’s all. Now you have 3 apps with different configurations and names from one project, and you are ready to test against various API’s, databases or whatever you have stored in your appsettings file.



Author
Szczepan Błaszkiewicz
Software Developer

I’m a .NET-driven full stack developer. I love the idea behind Docker and I’m happy to share it.