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
andAndroidManifest.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.
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.
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.
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.
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>
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>
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.
- Go to C:\Users{YourUserName}\AppData\Local\Xamarin\Mono for Android\Keystore and find a keystore you have created.
- Copy both files to your project directory.
- 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:
- Return to editing your configurations and select Copy configuration from the Action dropdown
- Change the value of the Version parameter to STAGE or PROD instead of DEV
- 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.