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:
[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:
public partial class AndroidTest
{
[ClassInitialize()]
public static void AndroidTestSettings(TestContext context)
{
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:
private static AndroidDevice GetFirstConnectedDevice()
{
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.");
}
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));
}
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));
}
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:
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);
}
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:
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:
String output = AndroidUtilities.LaunchCommandLineApp("adb", "shell rm -rf " + path);
I wrote another utility function for this second approach:
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);
}
processOutput = processOutput.Trim();
processOutput = processOutput.TrimEnd(System.Environment.NewLine.ToCharArray());
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:
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:
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.