通过 Windows C# 应用操控安卓设备






4.58/5 (8投票s)
本技巧介绍了如何编写一个 C# 应用程序来控制安卓设备。它使用了 Quamotion 提供的 MADB 封装库。
引言
你是否曾想过拥有一个在 Windows 上运行并能控制安卓设备的 C# 应用?这类应用的一个很酷的例子是测试运行器,它可以安装应用程序、执行它们,然后收集所有的测试结果。
本文将向你展示如何通过这样的 C# 应用来操控安卓设备。
入门
首先,你需要从 GitHub 安装 MADB 代码,请访问此网页并下载 zip 格式的源代码。
如果你不想自己构建这个库,可以直接使用包管理器工具。你需要将其作为 Visual Studio 的一个插件。
从以下链接获取你需要的 NuGet 包管理器版本:
在 Visual Studio 中安装好之后,打开 PM shell(工具->NuGet 包管理器->包管理器控制台),然后用这个命令来安装二进制文件。
PM> Install-Package madb
现在将 MADB 引用添加到你的项目中,它的名字是 managed.adb。
检查你是否有一台受支持的安卓设备。似乎没有一个明确的支持操作系统版本甚至设备的列表,所以直接试试看,如果它没有按预期工作,那么就将设备升级到最新的操作系统版本。
通过 USB 端口插入设备(不支持无线连接)。
下载安卓 IDE(Android Studio)和安卓调试桥工具(ADB)。
从下面的链接获取它们:
安装 IDE 和工具,并设置你的环境变量路径指向工具文件夹(开始->系统->关于->系统信息->高级系统设置->环境变量 - 编辑系统路径,添加路径并用分号与其他路径隔开),例如我的文件夹在这里:
PATH=C:\Users\Owner\AppData\Local\Android\sdk\tools;
C:\Users\Owner\AppData\Local\Android\sdk\platform-tools;C:\Pro
打开一个 Windows 命令 shell 并输入 path,以确保 ADB 工具路径已在其中。
当你完成所有设置后,尝试一些命令:
- adb devices - 这将列出所有通过 USB 连接的安卓设备。
- adb shell ls - 列出设备根目录下的所有文件。
- adb shell "pm list packages | grep marcus" - 列出设备上所有 URI 中包含单词 marcus 的已安装应用程序,例如 com.marcus.mytestapp。
如果你的 Windows shell 当前文件夹中有一个预构建的安卓应用程序包(.apk),那么你可以尝试这些命令:
- adb install <应用程序文件名> - 例如 C:/MyTespApp.apk
- adb uninstall <你的应用程序标识符> - 例如 com.marcus.mytestapp
- adb shell am start -n <你的应用程序标识符/你想要运行的活动> - 这将在设备上执行该应用并启动指定的活动。例如 com.my.app/com.my.app.MainActivity
注意,你可以在 shell 后面添加任何类似 linux 的(POSIX)命令,然后,叮!ADB 就会在设备上执行该命令。
但请注意,虽然你可以运行任何命令,但读/写/创建文件和目录的权限可能会很麻烦。设备默认不是一个读/写磁盘,它是只读的。所以执行一个 mkdir dave
命令,很可能会返回一个权限被拒绝的错误。
要试验设备以发现哪些操作是允许的,一个好方法是使用 shell 登录到设备上。
adb shell
这会让你通过 SSH 连接到设备上。在这里,尝试你的 POSIX 命令,看看它是否能工作。
Using the Code
首先确保你已经链接了对 madb C# 程序集包(managed.adb
)的引用。对于我们想做的大部分事情,我们将使用 Device
类的一个实例,所以请在任何文件的顶部添加这个 using
语句。
using Managed.Adb;
使用设备进行测试
当我编写针对外部设备运行的测试时,我喜欢尽可能地实现自动化。我认为让我的测试用户在运行测试前必须更改代码,例如设置一个序列号属性,是不合理的。我喜欢让事情变得简单的一个好例子是,预先配置测试,使其针对 adb devices
命令返回的第一个通过 USB 连接的安卓设备运行。
我采用测试驱动开发,所以我的测试总是优先。我喜欢将一个测试类分成两个文件(因此该类在每个文件中都是 partial
)。原因是我喜欢将我所有的测试放在一个文件中,而将设置和清理工作放在另一个文件中。
例如,包含我所有测试的文件看起来像这样:
[TestClass()]
public partial class AndroidTest
{
[TestMethod]
[Description("Test the power on is not supported ")]
public void AndroidTarget_Power_On()
{
Blah Blah Blah
}
}
而包含我的设置和清理代码的文件是:
public partial class AndroidTest
{
[ClassInitialize()]
public static void AndroidTestSettings(TestContext context)
{
//Get the details about the first connected device
CurrentDevice = GetFirstConnectedDevice();
Blah Blah Blah
}
}
所以这里是一个获取第一个连接的安卓设备序列号的 static
函数。注意它是如何从我的 MS Test - 带有 [ClassInitialize]
注解的函数中被调用的,这个函数在整个测试套件中只会被调用一次。
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());
}
}
你可能会注意到,在调试模式下,这还会将设备上的所有环境变量以及所有系统属性(例如设备名称、操作系统版本、CPU 类型、产品类型等)转储到日志文件中。这些都是可以从日志中获得的有用信息。
一旦我们获得了第一个设备,我们就可以使用它的序列号来调用任何外部的 adb
命令。
与安卓设备通信
要与安卓设备通信,我们希望 adb 能传递给我们正确的 Device
实例。在测试时,我们知道想要测试的序列号,现在我们只需要从 ADB 获取正确的 Device
实例。
这里有一个函数,它会给我们想要的实例:
/// <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;
}
有三种方式可以使用 managed adb 代码与设备通信。下面将详细介绍每种方式的用法。
调用设备函数
与设备通信的第一个机制是使用 adb Device
实例的特定函数。下面是一个如何重启设备的例子:
Device android = AndroidUtilities.ADBConnectedAndroidDevice(serialNumber);
android.Reboot();
以下是 Device
类支持的功能列表:
AvdName
- 获取设备名称GetBatteryInfo
- 获取电池信息InstallPackage
- 在设备上安装一个实际的包(.apk)IsOffline
- 获取设备的离线状态IsOnline
- 获取设备的在线状态Model
- 获取设备的型号Screenshot
- 抓取设备的当前屏幕截图SerialNumber
- 获取设备的序列号State
- 获取设备的状态UninstallPackage
- 从设备上卸载一个实际的包
通用 Shell (SSH) 命令
与安卓设备通信的第二种方法是使用 adb
作为到设备的 shell,为此我们可以传入任何支持的 POSIX 命令。
String output = AndroidUtilities.LaunchCommandLineApp("adb", "shell rm -rf " + path);
我为这第二种方法写了另一个工具函数:
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;
}
注意我在末尾去除不良字符的方式。XML 有时会包含一些导致 log4net 写入函数崩溃的字符,所以我在写入日志前将它们移除。
另外请注意,我在我的代码中定义了一个 TargetException
,你可以使用你自己的或者根本不用,如果你想的话,可以直接重新抛出异常。
关于可以传入此函数的有用命令列表,请参阅我的另一篇文章:
ExecuteShellCommand
如果我们想从设备上的一个命令获取多行返回结果,我们可以使用 Device
类上的 ExecuteShellCommand
。首先,我们需要建立一个类来捕获输出的行。下面是一个例子:
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);
}
}
有了这个类,我们现在可以调用该函数,下面是一个列出并输出所有文件的例子:
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);
}
}
因此,从这里开始,我们也可以调用任何我们想要的 shell 命令。
结论
Quamotion 提供了一个很棒的开源 C# ADB 封装库。本文中应该有足够的内容让你能够看到,通过 Windows 上的 C# 应用可以对安卓设备实现哪些功能。
我希望你觉得这很有用,或者至少有点意思!编程愉快。
谢谢
非常感谢 Quamotion 提供了一个免费、开源的 ADB 封装库,并特别感谢来自 Quamotion 的 Frederik Carlier 的所有帮助和指导。