65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.58/5 (8投票s)

2015年10月21日

CPOL

7分钟阅读

viewsIcon

82207

本技巧介绍了如何编写一个 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 的所有帮助和指导。

© . All rights reserved.