- 作者:老汪软件技巧
- 发表时间:2024-12-07 11:05
- 浏览量:
AASClient aasClient = AASClient();
Image.network(aasClient.iconUrl('com.nightmare'))
AASClient 是多实例,所有的 API 被封装到 AASClient 下
多实例可以让同一个页面加载不同设备的信息,例如 Uncon
获取所有应用信息代码
AppInfos infos = await getAllAppInfos(isSystemApp: false);
这看起来没什么,但我们可以将这个 Example 编译到 Mac 端
我们只需要更改端口号
AASClient aasClient = AASClient(port: Platform.isMacOS ? 15000 : null);
这就是在 API 使用上的一致性,我有各种同时运行在 App 内的页面,只需要更改端口号,就可以运行在 PC 端
当然 PC 端需要以 Dex Mode 启动 AAS
并且在这种模式下,我们可以有更多的 API 支持
可以获取到安卓的后台截图,这个功能应该是极少见到的
示例代码中包含了所有的 API 使用方法,示例代码是最好能理解 AAS 的方式
完整代码见 Flutter Example
启动模式介绍
通过这些例子你应该发现了,PC 端加载对应页面的时候,安卓端的服务是怎么启动的?
AAS 有两种启动模式
Activity Mode
这种情况下,AAS 拥有真实的 Activity Context,对于获取应用列表,同普通安卓本身访问 API一样,需要申请权限
Dex Mode
启动脚本在 build_and_run.sh
这种模式,会先将 java 编译成 class,再由 dx 或 d8 工具转换成 dex 文件
通过 adb 运行 app_process 启动 dex
这种模式带来的好处是,我们能使用的权限更多,例如获取后台任务缩略图,创建虚拟显示器(带 Group 的)
所有 java 的权限为 shell(uid 2000),你无需再为获取应用列表,创建虚拟显示器等单独申请权限
我们可以通过为连接到 PC 的设备启动这个服务,再通过 adb forward 获得通信的端口(auto.sh 中带有端口转发)
接下来,你仍然只需要像这样就获得 App的图标
AASClient aasClient = AASClient(port: port);
Image.network(aasClient.iconUrl('com.nightmare'))
你还可以自己实现各种各样的 API ,来获得远超 adb 命令行的功能,例如图标,后台应用截图,adb 命令本身不支持
在 Flutter 中使用
提供 android_api_server_client 来快速的让 Flutter App 拥有这个能力,无需手动启动服务,AAS 随 Flutter Plugin 注册而启动,直接创建 AASClient 则会使用 Flutter Plugin 中启动的端口
dependencies:
android_api_server_client:
git: https://github.com/nightmare-space/android_api_server_client
然后直接使用封装好的 Dart API
AASClient aasClient = AASClient();
AppInfos infos = await aasClient.getAppInfos();
如果你需要在 PC 上访问同样的接口,通过 Dex Mode,你只需要更改端口
AASClient aasClient = AASClient(port: 15000);
AppInfos infos = await aasClient.getAppInfos();
假如我目前有一个 Flutter 编写的展示应用列表的界面是这样
现在我想这个界面在 PC 上展示,亦或者在 Web 中展示
启动 Dex 后,我只需要修改端口号即可
AASClient aasClient = AASClient(port: Platform.isMacOS ? 15000 : null);
实际上,这样的模式已大量的在无界、速享、ADB KIT中使用
其中的文件管理器、应用列表、任务列表,都是完全的同一份代码,仅仅是端口号不一样
在原生安卓中使用
根据仓库的 Tag 版本,引入对应的依赖
implementation 'com.github.nightmare-space.android_api_server:aas_integrated:v0.1.27'
启动服务
AASIntegrate aasIntegrate = new AASIntegrate();
try {
int port = aasIntegrate.startServerFromActivity(context);
Log.d(TAG, "port -> " + port);
} catch (Exception e) {
Log.d(TAG, "error -> " + e);
e.printStackTrace();
}
所以通过这两种模式的了解,在 PC 端获取安卓的信息,通常需要 ADB KIT 或者 Uncon 中一样,需要处理 Dex 的启动流程
详见 ADB KIT
使用 AAS API 以及自定义插件自定义一个插件
这是获取安卓后台快照的完整插件
public class ActivityTaskManagerPlugin extends AndroidAPIPlugin {
@Override
public String route() {
return "/task_thumbnail";
}
public Bitmap graphicBufferToBitmap(GraphicBuffer graphicBuffer) {
int width = graphicBuffer.getWidth();
int height = graphicBuffer.getHeight();
int format = graphicBuffer.getFormat();
Bitmap.Config config;
if (format == PixelFormat.RGBA_8888) {
config = Bitmap.Config.ARGB_8888;
} else {
throw new IllegalArgumentException("Unsupported format: " + format);
}
Bitmap bitmap = Bitmap.createBitmap(width, height, config);
graphicBuffer.lockCanvas().drawBitmap(bitmap, 0, 0, null);
graphicBuffer.unlockCanvasAndPost(graphicBuffer.lockCanvas());
return bitmap;
}
@Override
public NanoHTTPD.Response handle(NanoHTTPD.IHTTPSession session) {
String id = session.getParms().get("id");
L.d("id -> " + id);
byte[] bytes = null;
try {
long start = System.currentTimeMillis();
IActivityTaskManager activityTaskManager = ActivityTaskManager.getService();
ReflectionHelper.listAllObject(activityTaskManager);
Object snapshot = null;
// Android 12/Android 15
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.S || Build.VERSION.SDK_INT == 35) {
L.d("S or VANILLA_ICE_CREAM");
snapshot = ReflectionHelper.invokeHiddenMethod(activityTaskManager, "getTaskSnapshot", Integer.parseInt(id), false);
L.d("snapshot -> " + snapshot);
}
// Android 13/Android 14
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.TIRAMISU || Build.VERSION.SDK_INT == Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
L.d("TIRAMISU or UPSIDE_DOWN_CAKE");
snapshot = ReflectionHelper.invokeHiddenMethod(activityTaskManager, "getTaskSnapshot", Integer.parseInt(id), false, false);
L.d("snapshot -> " + snapshot);
}
Object hardBuffer = ReflectionHelper.getHiddenField(snapshot, "mSnapshot");
L.d("hardBuffer -> " + hardBuffer);
Object colorSpace = ReflectionHelper.getHiddenField(snapshot, "mColorSpace");
//
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
HardwareBuffer hardwareBuffer = (HardwareBuffer) hardBuffer;
Bitmap bitmap = Bitmap.wrapHardwareBuffer(hardwareBuffer, (ColorSpace) colorSpace);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos);
bytes = baos.toByteArray();
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return newFixedLengthResponse(NanoHTTPD.Response.Status.OK, "image/png", new ByteArrayInputStream(bytes), bytes.length);
}
}
相关解释
如果是同一类插件,建议只实现一个 AndroidAPIPlugin然后增加 param 来区分,例如 action
调用隐藏 API
Object result = ReflectionHelper.invokeHiddenMethod(Object object, String methodName, Object... args);
获取隐藏字段
Object result = ReflectionHelper.getHiddenField(object, "$name");
获取 Context
Context context = ContextStore.getContext();
解释一下为什么放在了单例里面,由于 AAS 有两种模式,两种模式下 Context 的获取方式是不一样的,Activity Mode 是直接储存的 Activity Context,Dex Mode 是通过反射获取的 Context
获取 Service
ServiceManager 来自 aas_hidden_api
ServiceManager.getService("activity_task");
例如我们要一个 PackageManager 的 Service
IPackageManager 来自 aas_hidden_api,PackageManager 是系统的 API
IPackageManager pms = IPackageManager.Stub.asInterface(ServiceManager.getService("package"));
PackageManager pm = ContextStore.getContext().getPackageManager();
这两种方法的选择要根据场景来,如果 IXManager 能实现需求,就用这种,因为在 Dex Mode 中,Context 是不完整的,但是例如 DisplayManager 这种,IDisplayManager 的 API 没有 DisplayManager 好用
更多获取方法详见 SystemServerApi
构建实例
AndroidAPIServer server = new AndroidAPIServer();
注册插件
server.registerPlugin(new PackageManagerPlugin());
AAS Intergated 代码
public class AASIntegrate {
static public void main(String[] args) {
AndroidAPIServer server = new AndroidAPIServer();
server.startServerForShell(args);
registerRoutes(server);
Looper.loop();
}
public static int startServerFromActivity(Context context) {
AndroidAPIServer server = new AndroidAPIServer();
int port = server.startServerFromActivity(context);
registerRoutes(server);
return port;
}
private static void registerRoutes(AndroidAPIServer server) {
server.registerPlugin(new PackageManagerPlugin());
server.registerPlugin(new ChangeDisplayHandler());
server.registerPlugin(new DisplayManagerPlugin());
server.registerPlugin(new ActivityManagerPlugin());
server.registerPlugin(new ActivityTaskManagerPlugin());
server.registerPlugin(new FilePlugin());
}
}
开源仓库介绍更多场景介绍文件选择、应用选择(ADB KIT、Uncon、Fast Share)
启动器
可以在 PC 端启动安卓的 App,配合应用流转使用,可实现无需解锁手机,即可在 PC 上运行安卓上的软件
文件预览(Uncon)
视频极速缓冲播放,100G 的文件都能随意拉动进度条
无需安卓安装额外 App,仅需要开启 USB 调试
已有插件介绍ActivityManagerPluginActivityTaskManagerPluginDisplayManagerPluginPackageManagerPluginFilePlugin最后
不要给我私信参加任何活动
不要给我任何狗屁创作激励,社区就是被这狗屁激励搞砸的
我也不知道这次回归,能持续多久,也许被某一条评论恶心到,然后再次离开?
世间的变数太多了
几句话与大家共勉
我曾一度讨厌任何变成以流量为目的社区
我们编写文章,记录开发历程,分享经验,变成了,我要如何制造噱头,如何编写更有噱头的标题