偶尔用一下adb,因不常用, 经常遇到命令记不清的情况, 网上找了几个adb助手,都偏向于界面化,做的很好,但对学习adb本身帮助不大.所以搞个工具方便我这样一年用不了几次,或初学adb的人.
预期功能:
- 输入adb命令时能显示上下文提示
- 上下文内容可扩充
- 一次能执行多个命令
- 能同时对多个设备操作.
- 能显示手机屏幕,并可在电脑上进行简单的操作
其他考虑:
- 尽可能少的依赖环境,最好只一个可执行文件.
- 尽可能不依赖第三方库和数据库等
效果图:
关键点
-
一.显示上下文提示框
思路: 当输入"空格" 后,取光标最近的一个单词为关键字, 以此关键字筛选相关内容,并在当前光标下方弹出窗口显示. 此步骤需解决的主要问题:
a). 取关键字(因不想使用第三方控件, 所以使用TextBox作为输入框, 试过RichTextBox,刷新太慢,放弃了)
//注: box 为TextBox控件
int curLineIdx = box.GetLineIndexFromCharacterIndex(box.CaretIndex); //取当前行标
int lineBegin = box.GetCharacterIndexFromLineIndex(curLineIdx);
string lineText = box.GetLineText(curLineIdx); //取当前行的全部文本
if (box.CaretIndex < lineBegin) return;
int linePos = box.CaretIndex - lineBegin; //取光标位置
if (lineText.Length < linePos) return;
lineText = lineText.Substring(0, linePos); //截取从本行开始到当前光标的文本
string[] spor = { " ", "\t" };
string[] buf = lineText.Split(spor, StringSplitOptions.None);
string curToken = buf.Last(); //正在输入的字符(即: 当前行,距离光标最近的空格之后 的输入内容)
b). 取弹出窗口的位置
POINT pt;
NativeMethods.GetCaretPos(out pt); //调用 windows API , 取当前光标位置
IntPtr wnd = NativeMethods.GetHandle(this);
NativeMethods.ClientToScreen(wnd, ref pt);
pt.Y += this._inputboxLineHeight; // TextBox的行高
this._pop.Left = pt.X; //_pop为 window,即弹出的提示窗口
this._pop.Top = pt.Y;
this._pop.Show();
c). 取TextBox的行高. (因字体原因,例如电脑上没有默认的字体,可能导致行高不同, 所以不能设为固定值)
//注: tb为TextBox控件
Typeface tc = new Typeface(tb.FontFamily, tb.FontStyle, tb.FontWeight, tb.FontStretch);
FormattedText ft = new FormattedText("A", CultureInfo.CurrentCulture, FlowDirection.LeftToRight, tc, tb.FontSize, tb.Background); //随便输入一个字符,取它的实际高度
return (int)Math.Round(ft.Height, 0); //输入框的行高
-
二.上下文管理
思路: 上下文保存在文本文件中, 运行时用一个数组存储, 输入关键字后筛选出相关的内容即可,
a).上下文结构. (相当于一对多的二维表)
//上下文结构
public class AdbContextValue
{
public string KeyWord { get; set; } //关键字
public IEnumerable<ContextValue> Context { get; set; } //对应的列表
}
b). 上下文缓存
//上下文缓存
public class ContextCache
{
volatile static ContextCache _instance = null;
static readonly object locker = new object();
public static ContextCache Instance
{
get
{
lock (locker)
{
if (_instance == null) _instance = new ContextCache();
return _instance;
}
}
}
private ContextCache()
{
this._adbContextCache = LoadCacheFromCsv(); //读取本地的缓存文件
}
List<AdbContextValue> _adbContextCache;
//用关键字索引
public IEnumerable<ContextValue> this[string Key]
{
get
{
foreach (var item in this._adbContextCache)
{
if (item.KeyWord == Key)
return item.Context;
}
return null;
}
}
}
-
三.执行adb
思路: 用cmd执行adb命令, 取输出即可,(需处理中文,特殊符号等问题). 如果直接用Process 执行adb,参数传递有问题,返回值也不好取.
a). 用cmd执行adb.
public string ExecuteArgs(string args, AdbDevice device = null, int timeout = 10000)
{
if (string.IsNullOrEmpty(this._adbPath)) throw new Exception("请先在app.json中设置adb路径");
if (string.IsNullOrEmpty(args)) throw new ArgumentNullException("无效adb参数");
using (Process proc = new Process())
{
proc.StartInfo.FileName = "cmd.exe";
proc.StartInfo.UseShellExecute = false;
proc.StartInfo.RedirectStandardInput = true;
proc.StartInfo.RedirectStandardOutput = true;
proc.StartInfo.RedirectStandardError = true;
proc.StartInfo.CreateNoWindow = true;
proc.EnableRaisingEvents = true;
proc.Start();
string cmd = string.Empty;
if (device == null)
cmd = $"\"{ this._adbPath}\" {Encoding.Default.GetString(Encoding.UTF8.GetBytes(args.Replace("|", "||")))}";
else
cmd = $"\"{ this._adbPath}\" -s {device.DeviceID} {Encoding.Default.GetString(Encoding.UTF8.GetBytes(args.Replace("|", "||")))}";
proc.StandardInput.WriteLine("echo off");
proc.StandardInput.WriteLine(cmd);
proc.StandardInput.WriteLine("exit");
StringBuilder sb = new StringBuilder();
proc.ErrorDataReceived += (ss, ee) =>
{
if (!string.IsNullOrEmpty(ee.Data) && ee.Data.IndexOf("Microsoft") < 0 && ee.Data.IndexOf(this._head) < 0 && ee.Data != "exit")
sb.AppendLine(ee.Data);
};
proc.OutputDataReceived += (ss, ee) =>
{
if (!string.IsNullOrEmpty(ee.Data) && ee.Data.IndexOf("Microsoft") < 0 && ee.Data.IndexOf(this._head) < 0 && ee.Data != "exit")
sb.AppendLine(ee.Data);
};
proc.BeginErrorReadLine();
proc.BeginOutputReadLine();
proc.WaitForExit(timeout);
if (sb.Length > 2 && sb[sb.Length - 1] == '\n' && sb[sb.Length - 2] == '\r')
sb.Remove(sb.Length - 2, 2);
return sb.ToString();
}
}
-
四. 显示手机屏幕
思路: 用连续截屏的方式模拟手机屏幕. 同时结合屏幕坐标与shell input 模拟操作.
在取输出流的时候走了点弯路,有的设备正常,有的设备截屏无效,查了半天找不出原因, 后来截取各手机的黑屏并保存为文件,用WinHex对比才发现有微小的区别(按道理,相同分辨率的纯黑图片应该完全相同),可能是不同安卓版本导致的.
public byte[] ScreenShotByte(AdbDevice device = null)
{
if (string.IsNullOrEmpty(this._adbPath)) throw new Exception("请先在app.json中设置adb路径");
byte[] rev;
using (Process proc = new Process())
{
proc.StartInfo.FileName = this._adbPath;
proc.StartInfo.UseShellExecute = false;
proc.StartInfo.RedirectStandardOutput = true;
proc.StartInfo.CreateNoWindow = true;
string cmd = "shell screencap -p"; //不输出文件,减少IO时间,也避免频繁写文件影响硬盘寿命
if (device != null)
cmd = $"-s {device.DeviceID} {cmd}";
proc.StartInfo.Arguments = cmd;
proc.Start();
using (MemoryStream memstream = new MemoryStream())
{
byte[] buffer = new byte[1];
while (proc.StandardOutput.BaseStream.Read(buffer, 0, 1) > 0) //因StandardOutput.ReadToEnd返回的是带编码的数据,故直接取Stream
{
memstream.Write(buffer, 0, 1);
}
rev = memstream.ToArray();
}
proc.WaitForExit(5000);
}
if (rev != null && rev.Length>1024)
{
if (device != null && device.VerNum <= 6)
return FixAdbBytesB(rev); //不同设备输出格式不同, 测试机器不多,最低只有安卓4设备
else
return FixAdbBytesA(rev); //修正控制台输出的0xOA 0xOD问题
}
else
return null;
}
修正换行符问题
//旧手机的输出\r\r\n和\r\n都有, 可能是版本原因
private byte[] FixAdbBytesB(byte[] data)
{
int lenght = data.Length;
int endPos = lenght - 1;
int endPos2 = lenght - 2;
List<byte> list = new List<byte>();
for (int i = 0; i < lenght; i++)
{
if (i < endPos && data[i] == 0x0D && data[i + 1] == 0x0A)
{
list.Add(0x0A);
i++;
continue;
}
else if (i < endPos2 && data[i] == 0x0D && data[i + 1] == 0x0D && data[i + 2] == 0x0A)
{
list.Add(0x0A);
i += 2;
continue;
}
list.Add(data[i]);
}
return list.ToArray();
}
本想加一个自动分析设备可用命令的功能, 例如自动查询设备可用指令,并筛选掉没权限的, 查询设备上的app并自动扩充到该设备的上下文缓存等, 想了想觉得工作量太大,放弃了. 等哪天来劲头了再搞
自动脚本功能搞了一半也去掉了,因为觉得实际意义不大, 直接用bat文件处理更简单方便.
详见代码:
百度网盘 提取码: 8888