Xamarin Virtual Machine Configuration

From Funtoo
Jump to navigation Jump to search

I (Daniel Robbins) sometimes use Xamarin for mobile development. This page serves as documentation on how to configure an Android Virtual Machine from the command-line, as this documentation is not widely available online, and the Xamarin Google Emulator Manager GUI is not currently available (as it's in the process of being rewritten, at least if you use the Alpha Channel.)

It's also just handy to be able to do this from the command-line if necessary as it can give you more control, the ability to debug problems, and just see how it all works. This works for me as of October 3rd, 2017, but may change in the future as the Android SDK is updated.

I have been using the Alpha channel for Xamarin/Visual Studio releases and have found that everything now wants you to target API Level 25. However, I didn't have any API Level 25 virtual machines available, and the virtual machine creator GUI for Xamarin is currently being overhauled so I had no way to create them except via the command-line. Here's how I did it.

First, in Visual Studio for Mac 7.2 Preview, I went to Tools -> SDK Command Prompt. This opened a Terminal window in the Android SDK directory. This actually isn't the right directory to be in. Change to:

user $ cd tools/bin

You can now run avdmanager as follows:

user $ ./avdmanager

The use of AVDManager is non-intuitive and has changed a lot over various Android releases so the next steps can be tricky. Manual edits of XML files may be required! But the command we will eventually use, and after some tweaking should work, is this:

user $ ./avdmanager create avd -f -n drobbins_vm_x86 -k 'system-images;android-25;google-apis_playstore;x86' -b x86 -d 31
   Note

This command will likely not work just yet and may need customization for your environment. But I'm including it above because maybe you're lucky and it will work as-is, and if not, we will go through the steps below to make it work :)

The various options used above are as follows:

optionmeaning
-fReplace existing AVD if it already exists
-n drobbins_vm_x86Name the AVD image drobbins_vm_x86
-d 31Use Device ID 31
-k 'system-images;android-25;google_apis_playstore;x86'Use the specified 'Package path'
-b x86Specify ABI to use -- note that some hackery may be required for this 'x86' option to work.

Getting AVDManager to Actually Work

It's likely that the command will not work as-is on your system. We'll go through all the trickery used to get avdmanager to work for you. First, to see all the possible Devices you can emulate, use the following command:

root # ./avdmanager list device

You will see a lot of debug output followed by a list of devices, identified by integer ID. You should pick the one you want to use.

Next, you'll need to pick a valid "Package Path". To see all available ones, type:

root # ./avdmanager -n bogus -k nonexisting
... java tracebacks ...
Error: Package path is not valid. Valid system image paths are:ository...       
system-images;android-21;google_apis;x86
system-images;android-25;google_apis;x86_64
system-images;android-25;google_apis_playstore;x86
system-images;android-25;android-tv;x86
system-images;android-23;google_apis;x86
system-images;android-23;google_apis;armeabi-v7a
system-images;android-21;default;x86_64
system-images;android-25;google_apis;x86
system-images;android-25;google_apis;armeabi-v7a
system-images;android-21;android-tv;armeabi-v7a
system-images;android-21;google_apis;armeabi-v7a
system-images;android-21;android-tv;x86
system-images;android-21;default;x86
system-images;android-25;google_apis;arm64-v8a
system-images;android-21;google_apis;x86_64
system-images;android-21;default;armeabi-v7a
null

Yes, exactly as typed, avdmanager will output a lot of debug output and then show you a list of system images beginning with "system-images;". There you go -- there's your list of available system images :) Ones with "playstore" in them are especially handy, as it allows you to launch the Google Play Store and update a bunch of stuff.

The ABI Mess

Unfortunately for me, some of the XML (shipped with the Android SDK?) was invalid which prevented me from creating an x86 image. The symptom was that only a 'default' ABI was defined in the XML, but this 'default' ABI was not part of the system-images, creating a catch-22. To fix this, I first got my avdmanager command ready, and then I ran it with -b default and -b x86, and saw a bunch of Java tracebacks saying things like The value 'default' (or x86) of element 'abi' is not valid.. But I did notice some debug output going to the console that said Parsing followed by a long list of XML files. (The list actually looks like Parsing foo.xmlParsing bar.xml, etc., all stuck together. BUT, if you find the proper XML file that corresponds to the system image you are trying to install, you can edit it, like so:

user $ vim /Users/drobbins/Library/Developer/Xamarin/android-sdk-macosx/system-images/android-25/google_apis_playstore/x86/package.xml

And then, search for the text <abi>, and you will likely see <abi>default</abi>. You have found the bug in the XML. Make sure you are modifying the "x86 Atom System Image" part of the XML and change default to x86, save, and then run the command with the -b x86 option and all the planets are now in alignment and it should work!

You're Not Done Yet

At this point, the avdmanager command specified initially (maybe with some tweaks made by you) should complete successfully. You will find the created AVD image in ~/.android/avd:

user $ ls ~/.android/avd
Android_ARMv7a.avd		Android_Accelerated_x86.avd	drobbins_vm_x86.avd
Android_ARMv7a.ini		Android_Accelerated_x86.ini	drobbins_vm_x86.ini

it will also show up immediately in the Visual Studio for Mac VM drop-down. But you're not done yet! If you just start your VM from Visual Studio, the resolution will be too small. So you will need to start your VM from the command-line, as follows:

user $ cd ../../emulator (or just cd emulator if from a new SDK terminal)
user $ ./emulator -avd drobbins_vm_x86 -accel auto -skin 1080x1920

The key above is the -skin option. This used to be baked into the AVD image but now needs to be specified on the emulator command-line. You can pick any XxY resolution you'd like to use or specify a standard (imo confusing) ?VGA string. There you go! Visual Studio will now 'hook in' to the running emulator and use it for debugging and launching your apps.

   Important

Quick Tip -- to update Google Play Services, use a play_services image, go to the Play Store and update all the apps in your image, and then launch Chrome -- this should immediately prompt you to update Google Play Services to the latest version. Proceed, and now you have Google Play Services in your VM, all updated and ready to go! :)

Code Changes for API Level 25

It is a very good idea to have an Android VM at API Level 25 for testing, because some things in Android 7.1 are just plain different, and this will mean that you will need to modify your code -- yes, even your platform-independent code. The big change I noticed is that while in the past you could specify what device functionality you needed directly in AndroidManifest.xml, you now need to still do this BUT ALSO programatically request access to the device functionality in your code. Why? Because in Android 7.1, your app will not automatically get the device permissions defined in AndroidManifest.xml, and it will blow up if it doesn't request them via code. The approach I used with The Permissions NuGet Package for Location permissions is documented below:

    (csharp source code)
public class MainPage : ContentPage
    {

        public async Task<bool> HasLocationPermissions() {
            try
            {
                var status = await CrossPermissions.Current.CheckPermissionStatusAsync(Permission.Location);
                if (status != PermissionStatus.Granted)
                {
                    if (await CrossPermissions.Current.ShouldShowRequestPermissionRationaleAsync(Permission.Location))
                    {
                        await DisplayAlert("Need location", "Gunna need that location", "OK");
                    }

                    var results = await CrossPermissions.Current.RequestPermissionsAsync(new[] { Permission.Location });
                    status = results[Permission.Location];
                }

                if (status == PermissionStatus.Granted)
                    return true;
                else if (status != PermissionStatus.Unknown)
                    return false;

            }
            catch (Exception)
            {
                return false;
            }
            return false;
        }

        public MainPage()
         
        public bool IsLocationAvailable()
        {
            if (!CrossGeolocator.IsSupported)
                return false;

            return CrossGeolocator.Current.IsGeolocationAvailable;
        }

        protected override void OnAppearing()
        {
            // I have a sub-class of Application called App with a boolean IsInitialized 
            // property to track if we have done this since app was launched
            if (!((App)Application.Current).IsInitialized)
            {
                
                // I found it optimal to do this on the UI thread to ensure it runs 
                // before any UI elements that need Location perms get busy...
                Device.BeginInvokeOnMainThread(async () =>
                {
                    ((App)Application.Current).IsInitialized = true;
                    
                    bool has_location = await HasLocationPermissions();
                    if (!has_location)
                    {
                        await DisplayAlert("OH", "We're going to need location permissions", "OK");
                    }
                    else
                    {
                        if (IsLocationAvailable())
                        {
                            /// Important - we are using non-beta geolocator plugin 4.0.1. API changes with 5.x
                            Plugin.Geolocator.Abstractions.Position p = await CrossGeolocator.Current.GetLastKnownLocationAsync();
                            Position p2 = new Position(p.Latitude, p.Longitude);
                            
                            // this BeginInvokeOnMainThread is needed because Xamarin is so good at multi-threading that this
                            // guy escapes from the main thread if we do not use this special block here:
                            Device.BeginInvokeOnMainThread(() =>
                            {
                                map.MoveToRegion(MapSpan.FromCenterAndRadius(p2, Distance.FromMiles(50)));
                            });
                        }
                        else
                        {
                            await DisplayAlert("OH", "We're going to need geolocation permissions", "OK");
                        }
                    }
                });
            }
        }
 
    }
   Important

James Montemagno has a great blog post on the details behind this new permissions scheme here: https://montemagno.com/simplified-ios-android-runtime-permissions-with/ . Also be sure to look at the GitHub page for the Permissions Plugin: https://github.com/jamesmontemagno/PermissionsPlugin