Click here to Skip to main content
15,886,578 members
Articles / Mobile Apps / Android
Tip/Trick

Manipulating Android Devices from a Windows C# App

Rate me:
Please Sign up or sign in to vote.
4.58/5 (8 votes)
20 Oct 2015CPOL7 min read 79.2K   19   6
This tip covers how to write a C# application to control an Android device. It uses the MADB wrapper from Quamotion.

Introduction

Ever wanted to have a C# app that runs on Windows and can control an Android device? One cool example for such an app could be a test runner that can install applications, execute them, and then collect all the test results.

This paper will show you how to manipulate an Android device from such a C# app.

Getting Started

To get started, you need to install the MADB code from GitHub, go to this web page and download the zipped up source code:

If you don't want to build the library yourself, simply use the package manager utility. You need to have this as a plugin to Visual Studio.

Get the NuGet Package Manager version you need from the following link:

Once it is installed in Visual Studio, open up the PM shell (Tools->NuGet Package manager->Package Manager Console), and simply install the binaries with this command:

PM> Install-Package madb

Now add the MADB reference to your project, it's called managed.adb.

Check you have a supported Android device. There doesn't seem to be a definitive list of supported OS versions or even devices, so just give it a try, and if it's not working as expected, then upgrade the device to the latest OS version.

Plug in the device via a USB port (wireless is not supported).

Download the Android IDE (Android Studio), and the Android Debug Bridge tool (ADB).

Get them from the link below:

Install the IDE and tools, and set up your environment path variable to point to the tools folder (Start->System->About->System Info->Advanced System Settings->Environment Variables - edit the system path, add the path and separate it from the others with a semi colon), for example my folder was here:

PATH=C:\Users\Owner\AppData\Local\Android\sdk\tools;
C:\Users\Owner\AppData\Local\Android\sdk\platform-tools;C:\Pro

Open a Windows command shell and type path to make sure the ADB tools path is in there.

When you have everything set up, try some commands:

  • adb devices - This will list all the USB connected Android devices
  • adb shell ls - List all the files on the device in the root folder
  • adb shell "pm list packages | grep marcus" - List all the installed applications on the device with the word marcus in the URI ie com.marcus.mytestapp

If you have a pre-built Android application package (.ipa) in your Windows shells' current folder, then you can try these commands:

  • adb install <application filename> - E.g. C:/MyTespApp.apk
  • adb uninstall <identifier of your application> - E.g. com.marcus.mytestapp
  • adb shell am start -n <identifier of your application/the activity you want to run> - This will execute the app on the device and start the specified activity E.g. com.my.app/com.my.app.MainActivity

Notice how you can add any linux type (POSIX) command after the shell, and Bingo! ADB will execute that command(s) on the device.

Be aware though, although you can run anything, file and directory permissions for reading/writing/creating can prove troublesome. The device by default is not a read/write disk, it is read only. So performing a mkdir dave, will probably give you back a permission denied error.

A good way to experiment with the device, in order to discover what is permissible, is to hop onto the device with a shell.

adb shell

This gives you an SSH onto the device. From here, try your POSIX command and see if it works.

Using the Code

First make sure you have linked in the reference to the madb C# assembly package (managed.adb). We will be using an instance of the Device class for most of what we want, so add this using to the top of any files:

using Managed.Adb;

Testing With A Device

When I am writing tests that run against an external device, I like to automate as much as possible. I don't think it's reasonable for users of my tests to have to change the code before they run the tests, for example setting a Serial Number property. A good example of how I like to make things easy is to pre-configure the tests to run against the first USB connected Android device that comes back from the adb devices command.

I develop test driven, so my tests always come first. I like to have a test class that is split between two files (so the class is partial in each file). The reason is because I like to have all my tests in one file, and the setting up and tearing down in another file.

For example, the file with all my tests in looks like this:

C#
[TestClass()]
public partial class AndroidTest
{
   [TestMethod]
   [Description("Test the power on is not supported ")]
   public void AndroidTarget_Power_On()
   {
     Blah Blah Blah
   }
}

And the file with my setup and tear down:

C#
public partial class AndroidTest
{
    [ClassInitialize()]
    public static void AndroidTestSettings(TestContext context)
    {
       //Get the details about the first connected device
       CurrentDevice = GetFirstConnectedDevice();
       Blah Blah Blah
    }
}

So here is a static function that gets the serial number of the first connected Android device. Note the way it is called from my MS Test - [ClassInitiliaze] annotated function above, this gets called only once for the whole test suite:

C#
private static AndroidDevice GetFirstConnectedDevice()
       {
           //Get the first entry from the list of connected Android devices
           try
           {
               AndroidDebugBridge mADB = AndroidDebugBridge.CreateBridge
               (Environment.GetEnvironmentVariable("ANDROID_ROOT") +
               "\\platform-tools\\adb.exe", true);
               mADB.Start();

               List<Device> devices =
               AdbHelper.Instance.GetDevices(AndroidDebugBridge.SocketAddress);

               if (devices.Count < 1)
               {
                   Debug.Fail("Test start-up failed.
                   Please plug in a valid Android device.");
                   throw new SystemException("Failed to start Android tests.
                   There are no Android devices connected, please connect a validated Android device.");
               }

               //Print out all the device properties in the log file
               foreach (KeyValuePair<string, string> kv in devices[0].Properties)
               {
                   Logger.Instance.WriteDebug(String.Format("Properties for
                   Device : {0} Key {1} : {2}", devices[0].SerialNumber, kv.Key, kv.Value));
               }

               //Print out all the environment vars to the log file
               Dictionary<string, string> vars = devices[0].EnvironmentVariables;
               foreach (KeyValuePair<string, string> kv in vars)
               {
                   Logger.Instance.WriteDebug(String.Format("Environment variables
                   for Device : {0} Key {1} : {2}", devices[0].SerialNumber, kv.Key, kv.Value));
               }

               //Take the first device
               return new AndroidDevice()
               {
                   TargetName = devices[0].Product,
                   TargetSerialNumber = devices[0].SerialNumber
               };

           }
           catch (Exception exc)
           {
               Debug.Fail("Test start-up failed. Please install ADB and
               add the environment variable ANDROID_ROOT to point to the path
               with the platform-tools inside.Exception : " + exc.ToString());
               throw new SystemException("Failed to start Android tests.
               Android Debug Bridge is not installed. Exception : " + exc.ToString());
           }
       }

You may notice in debug mode this will also dump to the logfile all the environment variables on the device and also all the system properties (for example device name, OS version, CPU type, product type etc). This is all useful information available from the log.

Once we have the first device, we can use its serial number to call any external adb commands.

Communicating with the Android Device

To talk to an Android device, we want the adb to pass us the correct Device instance. When testing, we know the serial number we want to test against, now we just need to get the correct Device instance from ADB.

Here is a function that will give us the instance we desire:

C#
/// <summary>
/// Gets the ADB Android instance with the specified serial number, 
/// we use this instance to talk to the device and send it
/// all specified commands.
/// </summary>
public static Device ADBConnectedAndroidDevice(string serialNumber)
{
     try
     {
           AndroidDebugBridge mADB = AndroidDebugBridge.CreateBridge
           (Environment.GetEnvironmentVariable("ANDROID_ROOT") + 
           @"\platform-tools\adb.exe", true);
           mADB.Start();

           List<Device> devices = 
           AdbHelper.Instance.GetDevices(AndroidDebugBridge.SocketAddress);
           foreach (Device device in devices)
           {
               if (device.SerialNumber == serialNumber)
               {
                   Logger.Instance.WriteInfo("ADBConnectedAndroidDevice - 
                   	found specified device : " + serialNumber);
                   return device;
               }
            }
     }
     catch
     {
          String errorMessage = "ADBConnectedAndroidDevice 
          ADB failed to start or retrieve devices. 
          Attempting to find SN : " + serialNumber;
          Logger.Instance.WriteError(errorMessage);
          throw new SystemException(errorMessage);
     }

     //didnt find the device with the specified SN
     Logger.Instance.WriteInfo("ADBConnectedAndroidDevice failed to find device. 
     Has the device been disconnected or unavailable ? Please check the device. 
     Attempting to find SN : " + serialNumber);
     return null;
}

There are three ways to use the managed adb code to talk to the device. A detailed usage of each is now given below.

Calling Device Functions

The first mechanism for talking to the device is using the adb Device instance with a specific function. Here is an example of how to reboot the device:

C#
Device android = AndroidUtilities.ADBConnectedAndroidDevice(serialNumber);
android.Reboot();

Here is a list of what is supported on the Device class:

  • AvdName - Gets the device name
  • GetBatteryInfo - Gets information on the battery
  • InstallPackage - Installs an actual package (.apk) on the device
  • IsOffline - Gets the device's offline state
  • IsOnline - Gets the device's online state
  • Model - Gets the device's model
  • Screenshot - Grabs the device's current screen
  • SerialNumber - Gets the device's serial number
  • State - Gets the device's state
  • UninstallPackage - Un-installs an actual package from the device

Generic Shell (SSH) Commands

The second way to talk to the Android device is to use the adb as a shell to the device, for this we can pass in any supported POSIX command we want:

C#
String output = AndroidUtilities.LaunchCommandLineApp("adb", "shell rm -rf " + path);

I wrote another utility function for this second approach:

C#
public static String LaunchExternalExecutable(String executablePath, String arguments)
{
            if (String.IsNullOrWhiteSpace(executablePath) == true)
            {
                String errorMessage = String.Format(" Path is not valid. 
                LaunchExternalExecutable called with invalid argument executablePath was empty.");
                Logger.Instance.WriteError(errorMessage);
                throw new ArgumentNullException(errorMessage);
            }

            String processOutput = "";

            ProcessStartInfo startInfo = new ProcessStartInfo()
            {
                CreateNoWindow = false,
                UseShellExecute = false,
                FileName = executablePath,
                WindowStyle = ProcessWindowStyle.Hidden,
                Arguments = arguments,
                RedirectStandardOutput = true
            };

            try
            {
               using (Process exeProcess = Process.Start(startInfo))
               {
                   processOutput = exeProcess.StandardOutput.ReadToEnd();
               }
            }
            catch (SystemException exception)
            {
                String errorMessage = String.Format("LaunchExternalExecutable - 
                Device Failed to launch a tool with 
                executable path {0}. {1}", executablePath, exception.ToString());
                Logger.Instance.WriteError(errorMessage);
                throw new Exception(errorMessage);
            }

            //Strip off extra characters - spaces, carriage returns, end of line etc
            processOutput = processOutput.Trim();
            processOutput = processOutput.TrimEnd(System.Environment.NewLine.ToCharArray());

            //Without this next change any text that contains
            //{X} will crash the String.Format inside the logger
            processOutput = processOutput.Replace('{', '[');
            processOutput = processOutput.Replace('}', ']');

            Logger.Instance.WriteInfo("LaunchExternalExecutable called. 
            Output from tool : " + processOutput, "");
            return processOutput;
}

Notice the way I strip off bad characters at the end. XML sometimes has characters that blow up the log4net writing functions, so I take them off before writing to the log.

Also notice I have a TargetException defined in my code, you can use your own or none at all, just re-throw if you want.

For a list of useful commands to pass in to this function, see my other paper:

ExecuteShellCommand

If we want to get back lots of lines from a command on the device, we can use the ExecuteShellCommand on the Device class. First, we need to set up a class to capture the lines of output. Here is an example:

C#
public class AndroidMultiLineReceiver : MultiLineReceiver
{
        public List<string> Lines {get; set;}

        public AndroidMultiLineReceiver()
        {
            Lines = new List<String>();
        }

        protected override void ProcessNewLines(string[] lines)
        {
            foreach (var line in lines)
                Lines.Add(line);
        }
}

With this class, we can now call the function, here is an example to list and output all files:

C#
public void ListRootAndPrintOut(string fileName)
{
     Device android = AndroidUtilities.ADBConnectedAndroidDevice(m_hostIdentifier);
     var receiver = new AndroidMultiLineReceiver();
     android.ExecuteShellCommand("ls", receiver, 3000);

     foreach(var line in receiver.Lines)
     {
         System.Diagnostics.Debug.WriteLine("Marcus - " + line);    
     }
}

And so from here, we can also call any shell commands we want.

Conclusion

Quamotion has provided a great open source C# wrapper for ADB. There should be enough in this paper for you to be able to see what kind of things are possible with Android devices from a C# app on Windows.

I hope you found this useful, or in the least, slightly interesting! Happy programming.

Thanks

Many thanks to Quamotion for providing a free, open source ADB wrapper, and special thanks to Frederik Carlier from Quamotion for all his help and guidance.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Technical Lead
Canada Canada
I have a blog here

http://www.electricalengineeringschools.org/blog/

And my robotics web site is here :

http://www.roboticsfordreamers.com/

I have been a software engineer for over 20 years now. I'm an expert in writing scalable restful web API services, which I've been doing for over 7 years now for games companies such as EA and Ubisoft. I've also worked on several video games, including Skate 2 and 3, NHL, Need for Speed, various Assassins Creed games, Far Cry 3 and 4, Driver San Francisco and most recently with a team of 15 on EA's UFC free to play. On this latest project I designed, implemented, tested (including load tests in Gatling and JUnit on AWS), deployed and supported all the web services for the game client, supporting up to a half a million active sessions.

I am proficient at programming in C, C++, C#, Java etc. I've developed many types of games, applications, SDKs and web services in Linux, Unix, iOS, Android and Windows.

My spare time is spent teaching and lecturing computing languages and science. I have a PhD in Artificial Intelligence (specialising in knowledge representation - Semantic networks with inference engines).

Currently I am also building an A.I. general purpose robot. The brains behind it are a mix of 6 Raspberry Pi and Banana Pros, with 2 USB cameras, SATA drive, Router Switch with inter-pi Comms, 2 motorised track systems, plus various sensors including motion/pir/sound etc.

The six pi's are split according to functions, including eyes (image processing/recognition), ears (speech processing and simulated speech),motor (object avoidance, environment mapping, drives all movement), entertainment (web browsing, media playing etc), brain (knowledge system, and adaptive learning/memory systems) and development system ( logging, diagnostics, debugging).

I am available as a consultant, so if you need to get something out the door quick, or want to set down an expandable and resilient framework, ping me !

Comments and Discussions

 
QuestionControl device Pin
RobbKirk15-Sep-19 20:08
RobbKirk15-Sep-19 20:08 
Questionmanipulating android emulators Pin
Member 1308678531-Mar-17 23:15
Member 1308678531-Mar-17 23:15 
QuestionLink doesn't corresponding with the article Pin
Ra.mon.cin29-Feb-16 0:51
Ra.mon.cin29-Feb-16 0:51 
QuestionListen to new device Pin
Member 1223916610-Feb-16 9:54
Member 1223916610-Feb-16 9:54 
QuestionDoubt regarding deployment of one such app Pin
deostroll14-Jan-16 19:14
deostroll14-Jan-16 19:14 
AnswerRe: Doubt regarding deployment of one such app Pin
Member 961621615-Feb-16 3:54
Member 961621615-Feb-16 3:54 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.