@xenon-device-management/xenon 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/README.md +446 -0
  2. package/lib/package.json +207 -0
  3. package/lib/public/assets/Layouts-7IT8aFLI.js +11 -0
  4. package/lib/public/assets/Layouts-DPMls9vh.css +1 -0
  5. package/lib/public/assets/ai-settings-BbnfgdEx.js +11 -0
  6. package/lib/public/assets/apps-CRMrI4_p.js +16 -0
  7. package/lib/public/assets/apps-CcM77dgg.css +1 -0
  8. package/lib/public/assets/badge-B1nKs8zj.css +1 -0
  9. package/lib/public/assets/badge-CSvl5xIU.js +11 -0
  10. package/lib/public/assets/button-CJlKn4PZ.css +1 -0
  11. package/lib/public/assets/button-CvLaGFYj.js +26 -0
  12. package/lib/public/assets/calendar-6w-D6Oaw.js +6 -0
  13. package/lib/public/assets/clock-DcdeWBPr.js +6 -0
  14. package/lib/public/assets/cpu-DiSoXT9n.js +6 -0
  15. package/lib/public/assets/device-explorer-CajM63OJ.js +193 -0
  16. package/lib/public/assets/device-explorer-CxdUAoTL.css +1 -0
  17. package/lib/public/assets/index-ByQwMN5T.js +174 -0
  18. package/lib/public/assets/index-C1DBaoSh.js +1 -0
  19. package/lib/public/assets/index-qzCez_kk.css +1 -0
  20. package/lib/public/assets/lock-B23ibZmo.js +6 -0
  21. package/lib/public/assets/maintenance-settings-CirzA6yG.js +6 -0
  22. package/lib/public/assets/mouse-pointer-2-Cz76SHFb.js +6 -0
  23. package/lib/public/assets/plus-BBwlIevt.js +6 -0
  24. package/lib/public/assets/session-dashboard-C2k7FFv_.css +1 -0
  25. package/lib/public/assets/session-dashboard-HPDtwPOZ.js +62 -0
  26. package/lib/public/assets/settings-DrZsZwdc.js +1 -0
  27. package/lib/public/assets/trash-2-DQpvzJec.js +6 -0
  28. package/lib/public/assets/useSocket-Dxsqae2a.js +16 -0
  29. package/lib/public/assets/webhook-settings-CDPgsgkb.css +1 -0
  30. package/lib/public/assets/webhook-settings-Cp-B4Nrw.js +1 -0
  31. package/lib/public/assets/zap-DovP6iow.js +6 -0
  32. package/lib/public/favicon.ico +0 -0
  33. package/lib/public/favicon.png +0 -0
  34. package/lib/public/favicon.svg +9 -0
  35. package/lib/public/index.html +46 -0
  36. package/lib/public/logo.svg +17 -0
  37. package/lib/public/logo192.png +0 -0
  38. package/lib/public/logo512.png +0 -0
  39. package/lib/public/manifest.json +25 -0
  40. package/lib/public/robots.txt +3 -0
  41. package/lib/schema.json +348 -0
  42. package/lib/src/InternalHttpClient.js +212 -0
  43. package/lib/src/PluginContext.js +29 -0
  44. package/lib/src/XenonCapabilityManager.js +199 -0
  45. package/lib/src/app/index.js +167 -0
  46. package/lib/src/app/routers/apps.js +79 -0
  47. package/lib/src/app/routers/config.js +131 -0
  48. package/lib/src/app/routers/control.js +835 -0
  49. package/lib/src/app/routers/dashboard.js +301 -0
  50. package/lib/src/app/routers/grid.js +352 -0
  51. package/lib/src/app/routers/reservation.js +190 -0
  52. package/lib/src/app/routers/webhook.js +83 -0
  53. package/lib/src/app/swagger-docs.js +203 -0
  54. package/lib/src/app/swagger.js +366 -0
  55. package/lib/src/chromeUtils.js +148 -0
  56. package/lib/src/commands/handle.js +19 -0
  57. package/lib/src/commands/index.js +8 -0
  58. package/lib/src/config.js +73 -0
  59. package/lib/src/dashboard/asset-manager.js +84 -0
  60. package/lib/src/dashboard/commands.js +284 -0
  61. package/lib/src/dashboard/event-manager.js +699 -0
  62. package/lib/src/dashboard/services/app-service.js +134 -0
  63. package/lib/src/dashboard/services/failure-analysis-service.js +173 -0
  64. package/lib/src/dashboard/services/session-service.js +113 -0
  65. package/lib/src/data-service/CircuitBreaker.js +83 -0
  66. package/lib/src/data-service/config-service.js +155 -0
  67. package/lib/src/data-service/db.js +122 -0
  68. package/lib/src/data-service/device-service.js +320 -0
  69. package/lib/src/data-service/device-store.interface.js +2 -0
  70. package/lib/src/data-service/device-store.js +345 -0
  71. package/lib/src/data-service/pending-sessions-service.js +25 -0
  72. package/lib/src/data-service/pluginArgs.js +25 -0
  73. package/lib/src/data-service/prisma-service.js +31 -0
  74. package/lib/src/data-service/prisma-store.js +385 -0
  75. package/lib/src/data-service/queue-service.js +150 -0
  76. package/lib/src/data-service/web-config-service.js +130 -0
  77. package/lib/src/device-managers/AndroidDeviceManager.js +1155 -0
  78. package/lib/src/device-managers/ChromeDriverManager.js +68 -0
  79. package/lib/src/device-managers/HealthMonitorService.js +325 -0
  80. package/lib/src/device-managers/IOSDeviceManager.js +351 -0
  81. package/lib/src/device-managers/NodeDevices.js +82 -0
  82. package/lib/src/device-managers/android/AndroidStreamService.js +370 -0
  83. package/lib/src/device-managers/android/DeviceLockManager.js +45 -0
  84. package/lib/src/device-managers/cloud/CapabilityManager.js +26 -0
  85. package/lib/src/device-managers/cloud/Devices.js +86 -0
  86. package/lib/src/device-managers/iOSTracker.js +44 -0
  87. package/lib/src/device-managers/index.js +89 -0
  88. package/lib/src/device-managers/ios/IOSDiscoveryService.js +268 -0
  89. package/lib/src/device-managers/ios/IOSStreamService.js +893 -0
  90. package/lib/src/device-managers/ios/WDAClient.js +866 -0
  91. package/lib/src/device-utils.js +663 -0
  92. package/lib/src/enums/Capabilities.js +8 -0
  93. package/lib/src/enums/Cloud.js +11 -0
  94. package/lib/src/enums/Platform.js +9 -0
  95. package/lib/src/enums/SessionType.js +9 -0
  96. package/lib/src/enums/SocketEvents.js +15 -0
  97. package/lib/src/helpers/UniversalMjpegProxy.js +273 -0
  98. package/lib/src/helpers/index.js +229 -0
  99. package/lib/src/index.js +95 -0
  100. package/lib/src/interceptors/CommandInterceptor.js +524 -0
  101. package/lib/src/interfaces/ICloudManager.js +2 -0
  102. package/lib/src/interfaces/IDevice.js +2 -0
  103. package/lib/src/interfaces/IDeviceFilterOptions.js +2 -0
  104. package/lib/src/interfaces/IDeviceManager.js +2 -0
  105. package/lib/src/interfaces/IOptions.js +2 -0
  106. package/lib/src/interfaces/IPluginArgs.js +55 -0
  107. package/lib/src/interfaces/ISessionCapability.js +2 -0
  108. package/lib/src/logger.js +225 -0
  109. package/lib/src/plugin.js +244 -0
  110. package/lib/src/prisma.js +12 -0
  111. package/lib/src/profiling/AndroidAppProfiler.js +213 -0
  112. package/lib/src/proxy/wd-command-proxy.js +221 -0
  113. package/lib/src/scripts/generate-database-migration.js +59 -0
  114. package/lib/src/scripts/initialize-database.js +55 -0
  115. package/lib/src/scripts/install-go-ios.js +66 -0
  116. package/lib/src/scripts/prepare-prisma.js +89 -0
  117. package/lib/src/services/AICommandService.js +143 -0
  118. package/lib/src/services/AIService.js +466 -0
  119. package/lib/src/services/CleanupService.js +141 -0
  120. package/lib/src/services/EventBus.js +74 -0
  121. package/lib/src/services/InspectorService.js +395 -0
  122. package/lib/src/services/MetricsService.js +134 -0
  123. package/lib/src/services/NetworkConditioningService.js +173 -0
  124. package/lib/src/services/NotificationService.js +163 -0
  125. package/lib/src/services/RequestLogService.js +252 -0
  126. package/lib/src/services/ResourceIsolationService.js +122 -0
  127. package/lib/src/services/SecurityService.js +120 -0
  128. package/lib/src/services/ServerManager.js +284 -0
  129. package/lib/src/services/SessionHeartbeatService.js +158 -0
  130. package/lib/src/services/SessionLifecycleService.js +572 -0
  131. package/lib/src/services/SocketClient.js +71 -0
  132. package/lib/src/services/SocketServer.js +87 -0
  133. package/lib/src/services/TracingService.js +132 -0
  134. package/lib/src/services/VideoPipelineService.js +220 -0
  135. package/lib/src/services/healing/FuzzyXmlHealingProvider.js +333 -0
  136. package/lib/src/services/healing/HealEtalonService.js +98 -0
  137. package/lib/src/services/healing/HealedLocatorGenerator.js +132 -0
  138. package/lib/src/services/healing/HealingOrchestrator.js +165 -0
  139. package/lib/src/services/healing/LlmHealingProvider.js +77 -0
  140. package/lib/src/services/healing/OcrHealingProvider.js +119 -0
  141. package/lib/src/services/healing/ResilioTreeHealingProvider.js +100 -0
  142. package/lib/src/services/healing/VisualAiHealingProvider.js +90 -0
  143. package/lib/src/services/healing/types.js +12 -0
  144. package/lib/src/services/omni-vision/OmniVisionService.js +718 -0
  145. package/lib/src/services/omni-vision/VisionAssertionService.js +68 -0
  146. package/lib/src/sessions/CloudSession.js +42 -0
  147. package/lib/src/sessions/LocalSession.js +313 -0
  148. package/lib/src/sessions/RemoteSession.js +287 -0
  149. package/lib/src/sessions/SessionManager.js +238 -0
  150. package/lib/src/sessions/XenonSession.js +44 -0
  151. package/lib/src/types/CLIArgs.js +2 -0
  152. package/lib/src/types/CloudArgs.js +2 -0
  153. package/lib/src/types/CloudSchema.js +131 -0
  154. package/lib/src/types/DeviceType.js +2 -0
  155. package/lib/src/types/DeviceUpdate.js +2 -0
  156. package/lib/src/types/IOSDevice.js +2 -0
  157. package/lib/src/types/Platform.js +2 -0
  158. package/lib/src/types/SessionStatus.js +11 -0
  159. package/lib/src/validators/CapabilityValidator.js +93 -0
  160. package/lib/test/e2e/android/conf.spec.js +43 -0
  161. package/lib/test/e2e/android/conf2.spec.js +44 -0
  162. package/lib/test/e2e/android/conf3.spec.js +44 -0
  163. package/lib/test/e2e/e2ehelper.js +113 -0
  164. package/lib/test/e2e/hubnode/forward-request.spec.js +224 -0
  165. package/lib/test/e2e/hubnode/hubnode.spec.js +214 -0
  166. package/lib/test/e2e/ios/conf1.spec.js +39 -0
  167. package/lib/test/e2e/ios/conf2.spec.js +39 -0
  168. package/lib/test/e2e/plugin-harness.js +236 -0
  169. package/lib/test/e2e/plugin.spec.js +97 -0
  170. package/lib/test/e2e/telemetry_verification.spec.js +83 -0
  171. package/lib/test/e2e/video-recording-test.spec.js +63 -0
  172. package/lib/test/helpers/test-container.js +112 -0
  173. package/lib/test/integration/androidDevices.spec.js +137 -0
  174. package/lib/test/integration/cliArgs.js +73 -0
  175. package/lib/test/integration/ios/01iOSSimulator.spec.js +291 -0
  176. package/lib/test/integration/ios/02iOSDevices.spec.js +75 -0
  177. package/lib/test/integration/testHelpers.js +74 -0
  178. package/lib/test/unit/AndroidDeviceManager.spec.js +178 -0
  179. package/lib/test/unit/ChromeDriverManager.spec.js +26 -0
  180. package/lib/test/unit/CleanupService.spec.js +21 -0
  181. package/lib/test/unit/DeviceModel.spec.js +157 -0
  182. package/lib/test/unit/FuzzyXmlHealingProvider.test.js +294 -0
  183. package/lib/test/unit/GetAdbOriginal.js +42 -0
  184. package/lib/test/unit/HealingCascade.test.js +128 -0
  185. package/lib/test/unit/IOSDeviceManager.spec.js +261 -0
  186. package/lib/test/unit/RemoteIOs.spec.js +78 -0
  187. package/lib/test/unit/ResilioTreeHealingProvider.test.js +96 -0
  188. package/lib/test/unit/commands.spec.js +27 -0
  189. package/lib/test/unit/config.spec.js +27 -0
  190. package/lib/test/unit/device-service.spec.js +307 -0
  191. package/lib/test/unit/device-utils.spec.js +313 -0
  192. package/lib/test/unit/fixtures/device.config.js +4 -0
  193. package/lib/test/unit/fixtures/devices.js +89 -0
  194. package/lib/test/unit/helpers.spec.js +62 -0
  195. package/lib/test/unit/omni-vision.spec.js +100 -0
  196. package/lib/test/unit/plugin.spec.js +133 -0
  197. package/lib/tsconfig.tsbuildinfo +1 -0
  198. package/package.json +207 -0
  199. package/prisma/data.db +0 -0
  200. package/prisma/dev.db +0 -0
  201. package/prisma/dev.db-journal +0 -0
  202. package/prisma/migrations/20231011074725_initial_tables/migration.sql +47 -0
  203. package/prisma/migrations/20231226115334_update_session_log/migration.sql +2 -0
  204. package/prisma/migrations/20251204113710_add_video_recording_enabled/migration.sql +29 -0
  205. package/prisma/migrations/20251204132449_add_log_table/migration.sql +11 -0
  206. package/prisma/migrations/20251205050111_add_profiling_support/migration.sql +47 -0
  207. package/prisma/migrations/20251205050947_add_is_error_field/migration.sql +24 -0
  208. package/prisma/migrations/20260126201337_add_app_model/migration.sql +18 -0
  209. package/prisma/migrations/20260130115722_add_performance_trace_and_xenon_sync/migration.sql +2 -0
  210. package/prisma/migrations/20260130135114_add_device_models/migration.sql +57 -0
  211. package/prisma/migrations/20260130140655_make_systemport_optional/migration.sql +45 -0
  212. package/prisma/migrations/20260130140932_make_device_fields_optional/migration.sql +45 -0
  213. package/prisma/migrations/20260130141040_final_schema_fix/migration.sql +45 -0
  214. package/prisma/migrations/20260130143234_add_device_health_fields/migration.sql +4 -0
  215. package/prisma/migrations/20260130144921_add_failure_category/migration.sql +2 -0
  216. package/prisma/migrations/20260131151456_add_webhook_config/migration.sql +10 -0
  217. package/prisma/migrations/20260201094507_add_device_tags/migration.sql +11 -0
  218. package/prisma/migrations/20260201103410_add_managed_process/migration.sql +15 -0
  219. package/prisma/migrations/20260201140637_add_web_config/migration.sql +22 -0
  220. package/prisma/migrations/20260201162232_add_session_progress/migration.sql +2 -0
  221. package/prisma/migrations/20260201174231_add_total_healed_count/migration.sql +2 -0
  222. package/prisma/migrations/migration_lock.toml +3 -0
  223. package/prisma/schema.prisma +210 -0
  224. package/schema.json +348 -0
  225. package/scripts/build-xenon.sh +32 -0
  226. package/scripts/dev/debug-gemini.ts +44 -0
  227. package/scripts/generate-types-from-schema.js +86 -0
  228. package/scripts/install-compatible-driver.js +39 -0
@@ -0,0 +1,1155 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
19
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
20
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
21
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
22
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
23
+ };
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ var __metadata = (this && this.__metadata) || function (k, v) {
42
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
43
+ };
44
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
45
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
46
+ return new (P || (P = Promise))(function (resolve, reject) {
47
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
48
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
49
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
50
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
51
+ });
52
+ };
53
+ var __importDefault = (this && this.__importDefault) || function (mod) {
54
+ return (mod && mod.__esModule) ? mod : { "default": mod };
55
+ };
56
+ Object.defineProperty(exports, "__esModule", { value: true });
57
+ const helpers_1 = require("../helpers");
58
+ const appium_adb_1 = require("appium-adb");
59
+ const logger_1 = __importDefault(require("../logger"));
60
+ const lodash_1 = __importDefault(require("lodash"));
61
+ const support_1 = require("@appium/support");
62
+ const ChromeDriverManager_1 = __importDefault(require("./ChromeDriverManager"));
63
+ const typedi_1 = require("typedi");
64
+ const device_utils_1 = require("../device-utils");
65
+ const adbkit_1 = __importDefault(require("@devicefarmer/adbkit"));
66
+ const node_abort_controller_1 = require("node-abort-controller");
67
+ const async_wait_until_1 = __importDefault(require("async-wait-until"));
68
+ const NodeDevices_1 = __importDefault(require("./NodeDevices"));
69
+ const device_service_1 = require("../data-service/device-service");
70
+ const device_store_1 = require("../data-service/device-store");
71
+ const Devices_1 = __importDefault(require("./cloud/Devices"));
72
+ const DeviceLockManager_1 = require("./android/DeviceLockManager");
73
+ const PluginContext_1 = require("../PluginContext");
74
+ const typedi_2 = require("typedi");
75
+ let AndroidDeviceManager = class AndroidDeviceManager {
76
+ constructor(context) {
77
+ this.context = context;
78
+ this.log = logger_1.default.scope('AndroidManager');
79
+ this.adbAvailable = true;
80
+ this.abortControl = new Map();
81
+ this.tracker = undefined;
82
+ this.remoteTrackers = [];
83
+ this.getDeviceName = (adbInstance, udid) => __awaiter(this, void 0, void 0, function* () {
84
+ let deviceName = yield this.getDeviceProperty(yield adbInstance, udid, 'ro.product.name');
85
+ if (!deviceName || (deviceName && deviceName.trim() === '')) {
86
+ // If the device name is null or empty, try to get it from the Bluetooth manager.
87
+ deviceName = yield (yield adbInstance).adbExec([
88
+ '-s',
89
+ udid,
90
+ 'shell',
91
+ 'dumpsys',
92
+ 'bluetooth_manager',
93
+ '|',
94
+ 'grep',
95
+ 'name:',
96
+ '|',
97
+ 'cut',
98
+ '-c9-',
99
+ ]);
100
+ }
101
+ return deviceName;
102
+ });
103
+ }
104
+ get pluginArgs() {
105
+ return this.context.pluginArgs;
106
+ }
107
+ get hostPort() {
108
+ return this.context.port;
109
+ }
110
+ get nodeId() {
111
+ return this.context.nodeId;
112
+ }
113
+ initiateAbortControl(deviceUdid) {
114
+ const control = new node_abort_controller_1.AbortController();
115
+ this.abortControl.set(deviceUdid, control);
116
+ return control;
117
+ }
118
+ abort(deviceUdid) {
119
+ var _a;
120
+ (_a = this.abortControl.get(deviceUdid)) === null || _a === void 0 ? void 0 : _a.abort();
121
+ }
122
+ cancelAbort(deviceUdid) {
123
+ if (this.abortControl.has(deviceUdid)) {
124
+ this.abortControl.delete(deviceUdid);
125
+ }
126
+ }
127
+ getDevices(deviceTypes, existingDeviceDetails) {
128
+ return __awaiter(this, void 0, void 0, function* () {
129
+ var _a;
130
+ if (!this.adbAvailable) {
131
+ logger_1.default.info('adb is not available. So, returning empty list');
132
+ return [];
133
+ }
134
+ let devices = [];
135
+ try {
136
+ if ((_a = this.pluginArgs.cloud) === null || _a === void 0 ? void 0 : _a.cloudName) {
137
+ const cloud = new Devices_1.default(this.pluginArgs.cloud, devices, 'android');
138
+ return yield cloud.getDevices();
139
+ }
140
+ else {
141
+ devices = yield this.fetchAndroidDevices(existingDeviceDetails, this.pluginArgs);
142
+ logger_1.default.info(`Found ${devices.length} android devices`);
143
+ }
144
+ if (deviceTypes.androidDeviceType === 'real') {
145
+ return devices.filter((device) => {
146
+ console.log(`Filtering device ${device.udid}, type: ${device.deviceType}, expected real`);
147
+ return device.deviceType === 'real';
148
+ });
149
+ }
150
+ else if (deviceTypes.androidDeviceType === 'simulated') {
151
+ return devices.filter((device) => {
152
+ return device.deviceType === 'emulator';
153
+ });
154
+ // return both real and simulated (emulated) devices
155
+ }
156
+ else {
157
+ return devices;
158
+ }
159
+ }
160
+ catch (e) {
161
+ logger_1.default.error(`Error while getting android devices. Error: ${e instanceof Error ? e.message : e}`);
162
+ }
163
+ return [];
164
+ });
165
+ }
166
+ fetchAndroidDevices(existingDeviceDetails, pluginArgs) {
167
+ return __awaiter(this, void 0, void 0, function* () {
168
+ yield this.requireSdkRoot();
169
+ const connectedDevices = yield this.getConnectedDevices(pluginArgs);
170
+ const deviceProcessingPromises = [];
171
+ for (const [adbInstance, devices] of connectedDevices) {
172
+ logger_1.default.debug(`fetchAndroidDevices from host: ${adbInstance.adbRemoteHost || 'Local'}. Found ${devices.length} android devices`);
173
+ const devicesArray = devices;
174
+ for (const device of devicesArray) {
175
+ deviceProcessingPromises.push((() => __awaiter(this, void 0, void 0, function* () {
176
+ device.adbRemoteHost =
177
+ adbInstance.adbRemoteHost === null
178
+ ? this.pluginArgs.bindHostOrIp
179
+ : adbInstance.adbRemoteHost;
180
+ const existingDevice = existingDeviceDetails.find((dev) => dev.udid === device.udid && dev.host.includes(this.pluginArgs.bindHostOrIp));
181
+ if (existingDevice) {
182
+ logger_1.default.info(`Android Device details for ${device.udid} already available`);
183
+ return Object.assign(Object.assign({}, existingDevice), { busy: false });
184
+ }
185
+ else {
186
+ logger_1.default.info(`Android Device details for ${device.udid} not available. So querying now.`);
187
+ if (device.state === 'device') {
188
+ try {
189
+ return yield this.deviceInfo(device, adbInstance, this.pluginArgs, this.hostPort);
190
+ }
191
+ catch (e) {
192
+ logger_1.default.error(`Error while getting device info for ${device.udid}. Error: ${e}`);
193
+ return undefined;
194
+ }
195
+ }
196
+ else {
197
+ logger_1.default.info(`Device ${device.udid} is not in "device" state. So, ignoring.`);
198
+ return undefined;
199
+ }
200
+ }
201
+ }))());
202
+ }
203
+ }
204
+ const processedDevices = yield Promise.all(deviceProcessingPromises);
205
+ const availableDevices = [];
206
+ const seenUdids = new Set();
207
+ for (const dev of processedDevices) {
208
+ if (dev && !seenUdids.has(`${dev.udid}-${dev.adbRemoteHost}`)) {
209
+ availableDevices.push(dev);
210
+ seenUdids.add(`${dev.udid}-${dev.adbRemoteHost}`);
211
+ }
212
+ }
213
+ return availableDevices;
214
+ });
215
+ }
216
+ deviceInfo(device, adbInstance, pluginArgs, hostPort) {
217
+ return __awaiter(this, void 0, void 0, function* () {
218
+ var _a;
219
+ const systemPort = yield (0, helpers_1.getFreePort)();
220
+ const totalUtilizationTimeMilliSec = yield (0, device_utils_1.getUtilizationTime)(device.udid);
221
+ let deviceInfo;
222
+ try {
223
+ deviceInfo = yield Promise.all([
224
+ this.getDeviceVersion(adbInstance, device.udid),
225
+ this.isRealDevice(adbInstance, device.udid),
226
+ this.getDeviceName(adbInstance, device.udid),
227
+ ]);
228
+ }
229
+ catch (error) {
230
+ logger_1.default.info(`Error while getting base device info for ${device.udid}. Error: ${error}`);
231
+ return undefined;
232
+ }
233
+ const [sdk, realDevice, name] = deviceInfo;
234
+ // Base info is mandatory
235
+ if (lodash_1.default.isNil(sdk) || lodash_1.default.isNil(realDevice) || lodash_1.default.isNil(name)) {
236
+ logger_1.default.info(`Cannot get base device info for ${device.udid}. Skipping`);
237
+ return undefined;
238
+ }
239
+ let host;
240
+ if (adbInstance.adbHost != null) {
241
+ host = `http://${adbInstance.adbHost}:${adbInstance.adbPort}`;
242
+ }
243
+ else if (pluginArgs.remoteMachineProxyIP !== undefined) {
244
+ host = `http://${pluginArgs.remoteMachineProxyIP}:${hostPort}`;
245
+ }
246
+ else {
247
+ host = `http://${pluginArgs.bindHostOrIp}:${hostPort}`;
248
+ }
249
+ return {
250
+ adbRemoteHost: (_a = adbInstance.adbRemoteHost) !== null && _a !== void 0 ? _a : undefined,
251
+ adbPort: adbInstance.adbPort,
252
+ systemPort,
253
+ sdk: sdk !== null && sdk !== void 0 ? sdk : 'unknown',
254
+ realDevice,
255
+ name: name !== null && name !== void 0 ? name : 'unknown',
256
+ busy: false,
257
+ state: device.state,
258
+ udid: device.udid,
259
+ platform: 'android',
260
+ deviceType: realDevice ? 'real' : 'emulator',
261
+ host,
262
+ totalUtilizationTimeMilliSec: totalUtilizationTimeMilliSec,
263
+ sessionStartTime: 0,
264
+ userBlocked: false,
265
+ };
266
+ });
267
+ }
268
+ getAdditionalDeviceInfo(device) {
269
+ return __awaiter(this, void 0, void 0, function* () {
270
+ logger_1.default.info(`Fetching additional device info for ${device.udid} (Lazy Loading)`);
271
+ const { adbInstance } = yield this.getAdb();
272
+ if (!adbInstance)
273
+ return {};
274
+ const adb = device.adbRemoteHost
275
+ ? adbInstance.clone({
276
+ remoteAdbHost: device.adbRemoteHost,
277
+ adbPort: device.adbPort,
278
+ })
279
+ : adbInstance;
280
+ try {
281
+ const [chromeDriverPath, screenSize] = yield Promise.all([
282
+ this.getChromeVersion(adb, device.udid, this.pluginArgs),
283
+ this.getScreenSize(adb, device.udid),
284
+ ]);
285
+ return {
286
+ chromeDriverPath,
287
+ screenWidth: screenSize === null || screenSize === void 0 ? void 0 : screenSize.width,
288
+ screenHeight: screenSize === null || screenSize === void 0 ? void 0 : screenSize.height,
289
+ };
290
+ }
291
+ catch (err) {
292
+ logger_1.default.warn(`Failed to fetch additional info for ${device.udid}: ${err}`);
293
+ return {};
294
+ }
295
+ });
296
+ }
297
+ getScreenSize(adbInstance, udid) {
298
+ return __awaiter(this, void 0, void 0, function* () {
299
+ try {
300
+ const adb = yield adbInstance;
301
+ let stdout = yield DeviceLockManager_1.deviceLock.acquire(udid, () => __awaiter(this, void 0, void 0, function* () {
302
+ return yield adb.adbExec(['-s', udid, 'shell', 'wm', 'size'], { timeout: 10000 });
303
+ }));
304
+ const physicalMatch = /Physical size: (\d+)x(\d+)/.exec(stdout);
305
+ const overrideMatch = /Override size: (\d+)x(\d+)/.exec(stdout);
306
+ if (overrideMatch) {
307
+ logger_1.default.info(`Detected Override size for ${udid}: ${overrideMatch[1]}x${overrideMatch[2]}`);
308
+ return { width: overrideMatch[1], height: overrideMatch[2] };
309
+ }
310
+ if (physicalMatch) {
311
+ logger_1.default.info(`Detected Physical size for ${udid}: ${physicalMatch[1]}x${physicalMatch[2]}`);
312
+ return { width: physicalMatch[1], height: physicalMatch[2] };
313
+ }
314
+ // Fallback: dumpsys display
315
+ logger_1.default.debug(`wm size failed for ${udid}, trying dumpsys display`);
316
+ stdout = yield adb.adbExec([
317
+ '-s',
318
+ udid,
319
+ 'shell',
320
+ 'dumpsys',
321
+ 'display',
322
+ '|',
323
+ 'grep',
324
+ 'mBaseDisplayInfo',
325
+ ]);
326
+ const sizeMatch = /width=(\d+), height=(\d+)/.exec(stdout);
327
+ if (sizeMatch) {
328
+ return { width: sizeMatch[1], height: sizeMatch[2] };
329
+ }
330
+ }
331
+ catch (error) {
332
+ logger_1.default.error(`Error while getting screen size for ${udid}. Error: ${error}`);
333
+ }
334
+ return undefined;
335
+ });
336
+ }
337
+ getAdb() {
338
+ return __awaiter(this, void 0, void 0, function* () {
339
+ try {
340
+ if (!this.adb) {
341
+ try {
342
+ this.adb = yield appium_adb_1.ADB.createADB({});
343
+ }
344
+ catch (e) {
345
+ this.adbAvailable = false;
346
+ this.log.error('Could not find ADB');
347
+ }
348
+ if (process.env.NODE_ENV !== 'test') {
349
+ const client = adbkit_1.default.createClient();
350
+ this.tracker = yield client.trackDevices();
351
+ if (this.tracker && this.adb) {
352
+ const originalADBTracking = this.createLocalAdbTracker(this.tracker, this.adb);
353
+ yield originalADBTracking();
354
+ }
355
+ }
356
+ }
357
+ }
358
+ catch (e) {
359
+ logger_1.default.error(`Failed to initialize ADB: ${e}`);
360
+ this.adbAvailable = false;
361
+ }
362
+ return { adbInstance: this.adb, adbTracker: this.tracker };
363
+ });
364
+ }
365
+ getAdbForDevice(udid) {
366
+ return __awaiter(this, void 0, void 0, function* () {
367
+ const { adbInstance } = yield this.getAdb();
368
+ if (!adbInstance)
369
+ throw new Error('ADB is not available');
370
+ const device = yield device_store_1.DeviceStoreFactory.getStore().findDevice({ udid });
371
+ if (device && device.adbRemoteHost) {
372
+ logger_1.default.debug(`Using remote ADB instance for ${udid} at ${device.adbRemoteHost}:${device.adbPort}`);
373
+ return adbInstance.clone({
374
+ remoteAdbHost: device.adbRemoteHost,
375
+ adbPort: device.adbPort,
376
+ });
377
+ }
378
+ return adbInstance;
379
+ });
380
+ }
381
+ waitBootComplete(originalADB, udid) {
382
+ return __awaiter(this, void 0, void 0, function* () {
383
+ return yield (0, async_wait_until_1.default)(() => __awaiter(this, void 0, void 0, function* () {
384
+ try {
385
+ const bootStatus = (yield this.getDeviceProperty(originalADB, udid, 'init.svc.bootanim'));
386
+ if (!lodash_1.default.isNil(bootStatus) && !lodash_1.default.isEmpty(bootStatus) && bootStatus == 'stopped') {
387
+ this.log.info('Boot Completed!', udid);
388
+ return true;
389
+ }
390
+ }
391
+ catch (err) {
392
+ return false;
393
+ }
394
+ }), {
395
+ intervalBetweenAttempts: 2000,
396
+ timeout: 60 * 1000,
397
+ });
398
+ });
399
+ }
400
+ getConnectedDevices(pluginArgs) {
401
+ return __awaiter(this, void 0, void 0, function* () {
402
+ const deviceList = new Map();
403
+ const { adbInstance: originalADB } = yield this.getAdb();
404
+ if (!originalADB)
405
+ return deviceList;
406
+ deviceList.set(originalADB, yield originalADB.getConnectedDevices());
407
+ const adbRemote = pluginArgs.adbRemote;
408
+ if (adbRemote !== undefined && adbRemote.length > 0) {
409
+ const promises = adbRemote.map((value) => __awaiter(this, void 0, void 0, function* () {
410
+ const adbRemoteValue = value.split(':');
411
+ const adbHost = adbRemoteValue[0];
412
+ const adbPort = parseInt(adbRemoteValue[1]) || 5037;
413
+ const cloneAdb = originalADB.clone({
414
+ remoteAdbHost: adbHost,
415
+ adbPort,
416
+ });
417
+ const devices = yield cloneAdb.getConnectedDevices();
418
+ deviceList.set(cloneAdb, devices);
419
+ const remoteAdb = adbkit_1.default.createClient({
420
+ host: adbHost,
421
+ port: adbPort,
422
+ });
423
+ const remoteAdbTracking = yield this.createRemoteAdbTracker(remoteAdb, originalADB, value);
424
+ yield remoteAdbTracking();
425
+ }));
426
+ yield Promise.all(promises);
427
+ }
428
+ return deviceList;
429
+ });
430
+ }
431
+ onDeviceAdded(originalADB, device) {
432
+ return __awaiter(this, void 0, void 0, function* () {
433
+ if (!device || !device.id)
434
+ return;
435
+ const newDevice = { udid: device.id, state: device.type };
436
+ logger_1.default.info(`Device ${newDevice.udid} was plugged. Detail: ${JSON.stringify(newDevice)}`);
437
+ if (newDevice.state != 'offline') {
438
+ logger_1.default.info(`Device ${newDevice.udid} was plugged`);
439
+ this.initiateAbortControl(newDevice.udid);
440
+ let bootCompleted = false;
441
+ try {
442
+ yield this.waitBootComplete(originalADB, newDevice.udid);
443
+ bootCompleted = true;
444
+ }
445
+ catch (error) {
446
+ logger_1.default.info(`Device ${newDevice.udid} boot did not complete. Error: ${error}`);
447
+ }
448
+ if (!bootCompleted) {
449
+ logger_1.default.info(`Device ${newDevice.udid} boot did not complete in time. Ignoring`);
450
+ return;
451
+ }
452
+ this.cancelAbort(newDevice.udid);
453
+ const trackedDevice = yield this.deviceInfo(newDevice, originalADB, this.pluginArgs, this.hostPort);
454
+ if (!trackedDevice) {
455
+ logger_1.default.info(`Cannot get device info for ${newDevice.udid}. Skipping`);
456
+ return;
457
+ }
458
+ logger_1.default.info(`Adding device ${newDevice.udid} to list!`);
459
+ const deviceTracked = Object.assign(Object.assign({}, trackedDevice), { nodeId: this.nodeId });
460
+ if (this.pluginArgs.hub != undefined) {
461
+ logger_1.default.info(`Updating Hub with device ${newDevice.udid}`);
462
+ const nodeDevices = new NodeDevices_1.default(this.pluginArgs.hub, this.pluginArgs.tlsRejectUnauthorized);
463
+ yield nodeDevices.postDevicesToHub([deviceTracked], 'add');
464
+ }
465
+ // node also need a copy of devices, otherwise it cannot serve requests
466
+ yield (0, device_service_1.addNewDevice)([deviceTracked], this.pluginArgs.bindHostOrIp);
467
+ }
468
+ });
469
+ }
470
+ createLocalAdbTracker(tracker, originalADB) {
471
+ const pluginArgs = this.pluginArgs;
472
+ const adbTracker = () => __awaiter(this, void 0, void 0, function* () {
473
+ try {
474
+ tracker.on('add', (device) => __awaiter(this, void 0, void 0, function* () {
475
+ if (!device || !device.id)
476
+ return;
477
+ yield this.onDeviceAdded(originalADB, device);
478
+ }));
479
+ tracker.on('remove', (device) => __awaiter(this, void 0, void 0, function* () {
480
+ if (!device || !device.id)
481
+ return;
482
+ yield this.onDeviceRemoved(device, pluginArgs);
483
+ }));
484
+ tracker.on('change', (device) => __awaiter(this, void 0, void 0, function* () {
485
+ if (!device || !device.id)
486
+ return;
487
+ if (device.type === 'offline' || device.type === 'unauthorized') {
488
+ yield this.onDeviceRemoved(device, pluginArgs);
489
+ }
490
+ else {
491
+ yield this.onDeviceAdded(originalADB, device);
492
+ }
493
+ }));
494
+ tracker.on('end', () => {
495
+ logger_1.default.info('Tracking stopped');
496
+ });
497
+ }
498
+ catch (err) {
499
+ logger_1.default.error('Something went wrong:', err instanceof Error ? err.stack : err);
500
+ }
501
+ });
502
+ return adbTracker;
503
+ }
504
+ onDeviceRemoved(device, pluginArgs) {
505
+ return __awaiter(this, void 0, void 0, function* () {
506
+ const clonedDevice = {
507
+ udid: device['id'],
508
+ host: pluginArgs.bindHostOrIp,
509
+ state: device.type,
510
+ };
511
+ if (pluginArgs.hub != undefined) {
512
+ const nodeDevices = new NodeDevices_1.default(pluginArgs.hub, pluginArgs.tlsRejectUnauthorized);
513
+ yield nodeDevices.postDevicesToHub([clonedDevice], 'remove');
514
+ }
515
+ // node also need a copy of devices, otherwise it cannot serve requests
516
+ yield (0, device_service_1.removeDevice)([clonedDevice]);
517
+ this.abort(clonedDevice.udid);
518
+ });
519
+ }
520
+ /**
521
+ * Return and cache a tracker for remote adb. If tracker already exists for the given id, return the existing one.
522
+ * @param adbClient
523
+ * @param originalADB
524
+ * @param id
525
+ * @returns
526
+ */
527
+ createRemoteAdbTracker(adbClient, originalADB, id) {
528
+ return __awaiter(this, void 0, void 0, function* () {
529
+ let remoteTracker;
530
+ // get tracker from remoteTracker list if already exists
531
+ const existingTracker = this.remoteTrackers.find((tracker) => tracker.id === id);
532
+ if (!existingTracker) {
533
+ const newTracker = yield adbClient.trackDevices();
534
+ this.remoteTrackers.push({ id, tracker: newTracker });
535
+ remoteTracker = newTracker;
536
+ }
537
+ else {
538
+ remoteTracker = existingTracker.tracker;
539
+ }
540
+ const pluginArgs = this.pluginArgs;
541
+ const adbTracking = () => __awaiter(this, void 0, void 0, function* () {
542
+ try {
543
+ remoteTracker.on('add', (device) => __awaiter(this, void 0, void 0, function* () {
544
+ yield this.onDeviceAdded(originalADB, device);
545
+ }));
546
+ remoteTracker.on('remove', (device) => __awaiter(this, void 0, void 0, function* () {
547
+ yield this.onDeviceRemoved(device, pluginArgs);
548
+ }));
549
+ remoteTracker.on('change', (device) => __awaiter(this, void 0, void 0, function* () {
550
+ if (device.type === 'offline' || device.type === 'unauthorized') {
551
+ logger_1.default.info(`Device ${device.id} is ${device.type}. Removing from list`);
552
+ yield this.onDeviceRemoved(device, pluginArgs);
553
+ }
554
+ else {
555
+ yield this.onDeviceAdded(originalADB, device);
556
+ }
557
+ }));
558
+ remoteTracker.on('end', () => this.log.info('Tracking stopped'));
559
+ }
560
+ catch (err) {
561
+ this.log.error('Something went wrong:', err instanceof Error ? err.stack : err);
562
+ }
563
+ });
564
+ return adbTracking;
565
+ });
566
+ }
567
+ getChromeVersion(adbInstance, udid, pluginArgs) {
568
+ return __awaiter(this, void 0, void 0, function* () {
569
+ if (pluginArgs.skipChromeDownload) {
570
+ logger_1.default.warn('skipChromeDownload server arg is set; skipping Chromedriver installation.');
571
+ logger_1.default.warn('Android web/hybrid testing will not be possible without Chromedriver.');
572
+ return;
573
+ }
574
+ logger_1.default.debug('Getting package info for chrome');
575
+ const chromeDriverManager = typedi_1.Container.get(ChromeDriverManager_1.default);
576
+ let versionName = '';
577
+ try {
578
+ const stdout = yield (yield adbInstance).adbExec(['-s', udid, 'shell', 'dumpsys', 'package', 'com.android.chrome']);
579
+ const versionNameMatch = new RegExp(/versionName=([\d+.]+)/).exec(stdout);
580
+ if (versionNameMatch) {
581
+ versionName = versionNameMatch[1];
582
+ versionName = versionName.split('.')[0];
583
+ return yield chromeDriverManager.downloadChromeDriver(versionName);
584
+ }
585
+ }
586
+ catch (err) {
587
+ logger_1.default.warn(`Error '${err instanceof Error ? err.message : err}' while dumping package info`);
588
+ }
589
+ });
590
+ }
591
+ downloadChromeDriver(version) {
592
+ return __awaiter(this, void 0, void 0, function* () {
593
+ const instance = typedi_1.Container.get(ChromeDriverManager_1.default);
594
+ return yield instance.downloadChromeDriver(version);
595
+ });
596
+ }
597
+ getDeviceVersion(adbInstance, udid) {
598
+ return __awaiter(this, void 0, void 0, function* () {
599
+ return yield this.getDeviceProperty(adbInstance, udid, 'ro.build.version.release');
600
+ });
601
+ }
602
+ getDeviceProperty(adbInstance, udid, prop) {
603
+ return __awaiter(this, void 0, void 0, function* () {
604
+ try {
605
+ return yield (yield adbInstance).adbExec(['-s', udid, 'shell', 'getprop', prop]);
606
+ }
607
+ catch (error) {
608
+ logger_1.default.error(`Error while getting device property "${prop}" for ${udid}. Error: ${error}`);
609
+ }
610
+ });
611
+ }
612
+ isRealDevice(adbInstance, udid) {
613
+ return __awaiter(this, void 0, void 0, function* () {
614
+ const character = yield this.getDeviceProperty(adbInstance, udid, 'ro.build.characteristics');
615
+ return character !== 'emulator';
616
+ });
617
+ }
618
+ requireSdkRoot() {
619
+ return __awaiter(this, void 0, void 0, function* () {
620
+ const sdkRoot = (0, appium_adb_1.getSdkRootFromEnv)();
621
+ const docMsg = 'Read https://developer.android.com/studio/command-line/variables for more details';
622
+ if (lodash_1.default.isEmpty(sdkRoot)) {
623
+ throw new Error(`Neither ANDROID_HOME nor ANDROID_SDK_ROOT environment variable was exported. ${docMsg}`);
624
+ }
625
+ if (sdkRoot === undefined || !(yield support_1.fs.exists(sdkRoot))) {
626
+ throw new Error(`The Android SDK root folder '${sdkRoot}' does not exist on the local file system. ${docMsg}`);
627
+ }
628
+ const stats = yield support_1.fs.stat(sdkRoot);
629
+ if (!stats.isDirectory()) {
630
+ throw new Error(`The Android SDK root '${sdkRoot}' must be a folder. ${docMsg}`);
631
+ }
632
+ return sdkRoot;
633
+ });
634
+ }
635
+ tap(udid, x, y) {
636
+ return __awaiter(this, void 0, void 0, function* () {
637
+ logger_1.default.info(`Android Tap on ${udid} at ${x},${y}`);
638
+ const adb = yield this.getAdbForDevice(udid);
639
+ yield DeviceLockManager_1.deviceLock.acquire(udid, () => __awaiter(this, void 0, void 0, function* () {
640
+ yield adb.adbExec(['-s', udid, 'shell', 'input', 'tap', Math.round(x).toString(), Math.round(y).toString()], { timeout: 10000 });
641
+ }));
642
+ });
643
+ }
644
+ swipe(udid, x, y, endX, endY, duration) {
645
+ return __awaiter(this, void 0, void 0, function* () {
646
+ logger_1.default.info(`Android Swipe on ${udid}: (${x},${y}) -> (${endX},${endY}) duration: ${duration}`);
647
+ const adb = yield this.getAdbForDevice(udid);
648
+ yield DeviceLockManager_1.deviceLock.acquire(udid, () => __awaiter(this, void 0, void 0, function* () {
649
+ yield adb.adbExec([
650
+ '-s',
651
+ udid,
652
+ 'shell',
653
+ 'input',
654
+ 'swipe',
655
+ Math.round(x).toString(),
656
+ Math.round(y).toString(),
657
+ Math.round(endX).toString(),
658
+ Math.round(endY).toString(),
659
+ duration.toString(),
660
+ ], { timeout: 15000 });
661
+ }));
662
+ });
663
+ }
664
+ typeText(udid, text) {
665
+ return __awaiter(this, void 0, void 0, function* () {
666
+ logger_1.default.info(`Android TypeText on ${udid}: ${text}`);
667
+ const adb = yield this.getAdbForDevice(udid);
668
+ // Escape whitespace for shell input
669
+ const escapedText = text.replace(/ /g, '%s');
670
+ yield DeviceLockManager_1.deviceLock.acquire(udid, () => __awaiter(this, void 0, void 0, function* () {
671
+ yield adb.adbExec(['-s', udid, 'shell', 'input', 'text', escapedText], { timeout: 10000 });
672
+ }));
673
+ });
674
+ }
675
+ pressKey(udid, keyCode) {
676
+ return __awaiter(this, void 0, void 0, function* () {
677
+ logger_1.default.info(`Android PressKey on ${udid}: ${keyCode}`);
678
+ const adb = yield this.getAdbForDevice(udid);
679
+ yield DeviceLockManager_1.deviceLock.acquire(udid, () => __awaiter(this, void 0, void 0, function* () {
680
+ yield adb.adbExec(['-s', udid, 'shell', 'input', 'keyevent', keyCode.toString()], {
681
+ timeout: 10000,
682
+ });
683
+ }));
684
+ });
685
+ }
686
+ getPageSource(udid) {
687
+ return __awaiter(this, void 0, void 0, function* () {
688
+ logger_1.default.info(`Android getPageSource on ${udid}`);
689
+ const adb = yield this.getAdbForDevice(udid);
690
+ return yield DeviceLockManager_1.deviceLock.acquire(udid, () => __awaiter(this, void 0, void 0, function* () {
691
+ try {
692
+ const dumpPath = '/data/local/tmp/dump.xml';
693
+ yield adb.adbExec(['-s', udid, 'shell', 'uiautomator', 'dump', dumpPath], {
694
+ timeout: 15000,
695
+ });
696
+ const xml = yield adb.adbExec(['-s', udid, 'shell', 'cat', dumpPath], { timeout: 10000 });
697
+ return xml || '';
698
+ }
699
+ catch (err) {
700
+ logger_1.default.error(`Failed to get Android page source for ${udid}: ${err.message}`);
701
+ return '';
702
+ }
703
+ }));
704
+ });
705
+ }
706
+ installApp(udid, appPath) {
707
+ return __awaiter(this, void 0, void 0, function* () {
708
+ const { adbInstance } = yield this.getAdb();
709
+ if (!adbInstance)
710
+ throw new Error('ADB is not available');
711
+ yield adbInstance.adbExec(['-s', udid, 'install', '-r', appPath]);
712
+ });
713
+ }
714
+ uninstallApp(udid, bundleId) {
715
+ return __awaiter(this, void 0, void 0, function* () {
716
+ const { adbInstance } = yield this.getAdb();
717
+ if (!adbInstance)
718
+ throw new Error('ADB is not available');
719
+ yield adbInstance.adbExec(['-s', udid, 'uninstall', bundleId]);
720
+ });
721
+ }
722
+ /**
723
+ * Helper function to temporarily set the Android device's IME to Appium Settings.
724
+ * This is required on Android 10+ to bypass OS background clipboard restrictions.
725
+ */
726
+ withAppiumIME(adbInstance, udid, action) {
727
+ return __awaiter(this, void 0, void 0, function* () {
728
+ let originalIME = '';
729
+ const appiumIME = 'io.appium.settings/.AppiumIME';
730
+ try {
731
+ // 1. Get current IME
732
+ originalIME = (yield adbInstance.adbExec([
733
+ '-s',
734
+ udid,
735
+ 'shell',
736
+ 'settings',
737
+ 'get',
738
+ 'secure',
739
+ 'default_input_method',
740
+ ])).trim();
741
+ // 2. Enable and Set Appium IME
742
+ if (originalIME !== appiumIME) {
743
+ yield adbInstance.adbExec(['-s', udid, 'shell', 'ime', 'enable', appiumIME]);
744
+ yield adbInstance.adbExec(['-s', udid, 'shell', 'ime', 'set', appiumIME]);
745
+ // Give the OS a tiny moment to process the IME swap
746
+ yield new Promise((resolve) => setTimeout(resolve, 500));
747
+ }
748
+ // 3. Execute the clipboard action with the Appium IME active
749
+ return yield action();
750
+ }
751
+ finally {
752
+ // 4. Always restore the original IME if we swapped it
753
+ if (originalIME && originalIME !== appiumIME) {
754
+ try {
755
+ yield adbInstance.adbExec(['-s', udid, 'shell', 'ime', 'set', originalIME]);
756
+ }
757
+ catch (e) {
758
+ logger_1.default.warn(`[${udid}] Failed to restore original IME (${originalIME}): ${e}`);
759
+ }
760
+ }
761
+ }
762
+ });
763
+ }
764
+ getClipboard(udid) {
765
+ return __awaiter(this, void 0, void 0, function* () {
766
+ const { adbInstance } = yield this.getAdb();
767
+ if (!adbInstance)
768
+ return '';
769
+ try {
770
+ // Wrap the targeted broadcast in the Appium IME context
771
+ return yield this.withAppiumIME(adbInstance, udid, () => __awaiter(this, void 0, void 0, function* () {
772
+ // 1. Try Targeted Broadcast method (Reliable for modern Android)
773
+ const result = yield adbInstance.adbExec([
774
+ '-s',
775
+ udid,
776
+ 'shell',
777
+ 'am',
778
+ 'broadcast',
779
+ '-a',
780
+ 'com.appium.settings.clipboard.get',
781
+ '-n',
782
+ 'io.appium.settings/.receivers.ClipboardReceiver',
783
+ ]);
784
+ // Parse result like: Broadcast completed: result=-1, data="BASE64_DATA"
785
+ const dataMatch = /data="([^"]*)"/.exec(result);
786
+ if (dataMatch) {
787
+ const rawData = dataMatch[1];
788
+ if (!rawData)
789
+ return '';
790
+ // Appium Settings returns Base64 for robustness
791
+ try {
792
+ const decoded = Buffer.from(rawData, 'base64').toString('utf8');
793
+ // If it looks like printable text after decoding, use it
794
+ if (/^[\x20-\x7E\s\u00A0-\uFFFF]*$/.test(decoded))
795
+ return decoded;
796
+ return rawData;
797
+ }
798
+ catch (e) {
799
+ return rawData;
800
+ }
801
+ }
802
+ // 2. Fallback: Query the content provider (Legacy/Alternative)
803
+ const queryResult = yield adbInstance.adbExec([
804
+ '-s',
805
+ udid,
806
+ 'shell',
807
+ 'content',
808
+ 'query',
809
+ '--uri',
810
+ 'content://io.appium.settings.clipboard/clipboard',
811
+ ]);
812
+ // Extract value using a more flexible regex that handles different formats
813
+ const valMatch = /value=([^\s,]*)/i.exec(queryResult);
814
+ if (valMatch) {
815
+ const val = valMatch[1];
816
+ // Most content providers return base64 for safety
817
+ try {
818
+ const decoded = Buffer.from(val, 'base64').toString('utf8');
819
+ // Basic sanity check: if it contains non-printable characters, it might not have been base64
820
+ if (/^[\x20-\x7E\s]*$/.test(decoded))
821
+ return decoded;
822
+ return val;
823
+ }
824
+ catch (e) {
825
+ return val;
826
+ }
827
+ }
828
+ return '';
829
+ }));
830
+ }
831
+ catch (err) {
832
+ logger_1.default.warn(`Failed to fetch Android clipboard for ${udid}: ${err instanceof Error ? err.message : err}`);
833
+ }
834
+ return '';
835
+ });
836
+ }
837
+ setClipboard(udid, content) {
838
+ return __awaiter(this, void 0, void 0, function* () {
839
+ const { adbInstance } = yield this.getAdb();
840
+ if (!adbInstance)
841
+ return;
842
+ try {
843
+ yield this.withAppiumIME(adbInstance, udid, () => __awaiter(this, void 0, void 0, function* () {
844
+ yield adbInstance.adbExec([
845
+ '-s',
846
+ udid,
847
+ 'shell',
848
+ 'am',
849
+ 'broadcast',
850
+ '-a',
851
+ 'com.appium.settings.clipboard.set',
852
+ '-n',
853
+ 'io.appium.settings/.receivers.ClipboardReceiver',
854
+ '--es',
855
+ 'label',
856
+ 'clipboard',
857
+ '--es',
858
+ 'content',
859
+ Buffer.from(content).toString('base64'), // Send as Base64 for safety
860
+ ]);
861
+ }));
862
+ }
863
+ catch (err) {
864
+ logger_1.default.warn(`Failed to set Android clipboard for ${udid}: ${err instanceof Error ? err.message : err}`);
865
+ }
866
+ });
867
+ }
868
+ touchAndHold(udid, x, y, duration) {
869
+ return __awaiter(this, void 0, void 0, function* () {
870
+ const { adbInstance } = yield this.getAdb();
871
+ if (!adbInstance)
872
+ return;
873
+ yield DeviceLockManager_1.deviceLock.acquire(udid, () => __awaiter(this, void 0, void 0, function* () {
874
+ yield adbInstance.adbExec([
875
+ '-s',
876
+ udid,
877
+ 'shell',
878
+ 'input',
879
+ 'swipe',
880
+ x.toString(),
881
+ y.toString(),
882
+ x.toString(),
883
+ y.toString(),
884
+ duration.toString(),
885
+ ], { timeout: 15000 });
886
+ }));
887
+ });
888
+ }
889
+ lock(udid) {
890
+ return __awaiter(this, void 0, void 0, function* () {
891
+ const { adbInstance } = yield this.getAdb();
892
+ if (!adbInstance)
893
+ return;
894
+ // 26 is POWER button, usually locks if screen is on
895
+ yield adbInstance.adbExec(['-s', udid, 'shell', 'input', 'keyevent', '26']);
896
+ });
897
+ }
898
+ unlock(udid) {
899
+ return __awaiter(this, void 0, void 0, function* () {
900
+ const { adbInstance } = yield this.getAdb();
901
+ if (!adbInstance)
902
+ return;
903
+ // Wake up the device and potentially unlock
904
+ yield adbInstance.adbExec(['-s', udid, 'shell', 'input', 'keyevent', '224']); // WAKEUP
905
+ yield adbInstance.adbExec(['-s', udid, 'shell', 'input', 'keyevent', '82']); // MENU (locks/unlocks some devices)
906
+ });
907
+ }
908
+ listApps(udid) {
909
+ return __awaiter(this, void 0, void 0, function* () {
910
+ const { adbInstance } = yield this.getAdb();
911
+ if (!adbInstance)
912
+ return [];
913
+ // List all third-party apps
914
+ const stdout = yield adbInstance.adbExec(['-s', udid, 'shell', 'pm', 'list', 'packages', '-3']);
915
+ return stdout
916
+ .split(/\r?\n/)
917
+ .map((line) => line.replace(/^package:/i, '').trim())
918
+ .filter((line) => line.length > 0)
919
+ .sort();
920
+ });
921
+ }
922
+ getScreenshot(udid) {
923
+ return __awaiter(this, void 0, void 0, function* () {
924
+ logger_1.default.info(`Android Get Screenshot on ${udid}`);
925
+ // Principal Optimization: Try to use the latest frame from the active stream if available
926
+ try {
927
+ const { default: AndroidStreamService } = yield Promise.resolve().then(() => __importStar(require('./android/AndroidStreamService')));
928
+ const streamSession = typedi_1.Container.get(AndroidStreamService).getStreamStatus(udid);
929
+ if ((streamSession === null || streamSession === void 0 ? void 0 : streamSession.status) === 'running' && streamSession.latestFrame) {
930
+ // Sanity Check: A full screenshot should be at least ~10KB as JPEG.
931
+ // Staleness Check: If the frame is older than 5 seconds, it's considered poor quality for interactive use.
932
+ const isFresh = streamSession.latestFrameTimestamp &&
933
+ Date.now() - streamSession.latestFrameTimestamp < 5000;
934
+ if (streamSession.latestFrame.length > 10000 && isFresh) {
935
+ logger_1.default.info(`[AndroidDeviceManager] Using fresh cached stream frame for ${udid} screenshot.`);
936
+ return streamSession.latestFrame.toString('base64');
937
+ }
938
+ else if (!isFresh) {
939
+ logger_1.default.warn(`[AndroidDeviceManager] Cached frame for ${udid} is STALE (${Math.round((Date.now() - (streamSession.latestFrameTimestamp || 0)) / 1000)}s old). Falling back to direct screencap.`);
940
+ }
941
+ else {
942
+ logger_1.default.warn(`[AndroidDeviceManager] Cached frame for ${udid} is too small (${streamSession.latestFrame.length}b). Falling back to direct screencap.`);
943
+ }
944
+ }
945
+ }
946
+ catch (e) {
947
+ logger_1.default.debug(`Failed to check stream status for ${udid}: ${e}`);
948
+ }
949
+ const { spawn } = yield Promise.resolve().then(() => __importStar(require('child_process')));
950
+ const adb = yield this.getAdbForDevice(udid);
951
+ try {
952
+ // 1. Try targeted exec-out screencap -p for maximum speed/reliability
953
+ // This is binary-safe and avoids saving to device disk
954
+ const screenshot = yield DeviceLockManager_1.deviceLock.acquire(udid, () => __awaiter(this, void 0, void 0, function* () {
955
+ return yield new Promise((resolve, reject) => {
956
+ const adbArgs = ['-s', udid];
957
+ // remote ADB support
958
+ if (adb.adbHost && adb.adbPort) {
959
+ adbArgs.unshift('-H', adb.adbHost, '-P', adb.adbPort.toString());
960
+ }
961
+ const adbPath = adb.executable.path || 'adb';
962
+ const proc = spawn(adbPath, [...adbArgs, 'exec-out', 'screencap', '-p']);
963
+ const chunks = [];
964
+ proc.stdout.on('data', (c) => chunks.push(c));
965
+ proc.on('close', (code) => {
966
+ if (code === 0) {
967
+ const base64 = Buffer.concat(chunks).toString('base64');
968
+ logger_1.default.info(`ADB screencap successful for ${udid} (${base64.length} chars)`);
969
+ resolve(base64);
970
+ }
971
+ else {
972
+ reject(new Error(`ADB screencap failed with code ${code}`));
973
+ }
974
+ });
975
+ proc.on('error', (err) => {
976
+ logger_1.default.error(`ADB spawn error: ${err.message}`);
977
+ reject(err);
978
+ });
979
+ // Safety timeout
980
+ setTimeout(() => {
981
+ proc.kill();
982
+ reject(new Error('ADB Screenshot timeout after 15s'));
983
+ }, 15000);
984
+ });
985
+ }));
986
+ return screenshot;
987
+ }
988
+ catch (err) {
989
+ logger_1.default.error(`Failed to take screenshot for ${udid}: ${err instanceof Error ? err.message : err}`);
990
+ // Fallback: Try shell method with base64 conversion on device
991
+ try {
992
+ logger_1.default.info(`Attempting fallback screenshot for ${udid}...`);
993
+ const base64 = yield adb.adbExec(['-s', udid, 'shell', 'screencap', '-p', '|', 'base64']);
994
+ return base64.replace(/\r?\n/g, '');
995
+ }
996
+ catch (fallbackErr) {
997
+ logger_1.default.error(`Fallback screenshot also failed for ${udid}: ${fallbackErr instanceof Error ? fallbackErr.message : fallbackErr}`);
998
+ }
999
+ return '';
1000
+ }
1001
+ });
1002
+ }
1003
+ getLogs(udid) {
1004
+ return __awaiter(this, void 0, void 0, function* () {
1005
+ const { adbInstance } = yield this.getAdb();
1006
+ if (!adbInstance)
1007
+ return 'ADB is not available';
1008
+ try {
1009
+ // Get last 500 lines of logcat
1010
+ return yield adbInstance.adbExec([
1011
+ '-s',
1012
+ udid,
1013
+ 'shell',
1014
+ 'logcat',
1015
+ '-d',
1016
+ '-t',
1017
+ '500',
1018
+ '-v',
1019
+ 'threadtime',
1020
+ ]);
1021
+ }
1022
+ catch (err) {
1023
+ logger_1.default.warn(`Failed to fetch Android logs for ${udid}: ${err instanceof Error ? err.message : err}`);
1024
+ return `Failed to fetch logs: ${err instanceof Error ? err.message : err}`;
1025
+ }
1026
+ });
1027
+ }
1028
+ checkHealth(device) {
1029
+ return __awaiter(this, void 0, void 0, function* () {
1030
+ if (device.cloud)
1031
+ return { healthStatus: 'Healthy' };
1032
+ try {
1033
+ const adb = yield this.getAdbForDevice(device.udid);
1034
+ const [bootCompleted, batteryInfo, storageInfo] = yield Promise.all([
1035
+ adb.adbExec(['-s', device.udid, 'shell', 'getprop', 'sys.boot_completed']),
1036
+ adb.adbExec(['-s', device.udid, 'shell', 'dumpsys', 'battery']),
1037
+ adb.adbExec(['-s', device.udid, 'shell', 'df', '-h', '/data']),
1038
+ ]);
1039
+ const isBooted = bootCompleted && bootCompleted.trim() === '1';
1040
+ // Parse battery
1041
+ const batteryLevelMatch = /level: (\d+)/.exec(batteryInfo);
1042
+ const batteryLevel = batteryLevelMatch ? parseInt(batteryLevelMatch[1]) : undefined;
1043
+ const batteryTempMatch = /temperature: (\d+)/.exec(batteryInfo);
1044
+ const batteryTemp = batteryTempMatch ? parseInt(batteryTempMatch[1]) / 10 : undefined;
1045
+ let thermalStatus = 'Normal';
1046
+ if (batteryTemp && batteryTemp > 45)
1047
+ thermalStatus = 'Hot';
1048
+ if (batteryTemp && batteryTemp > 55)
1049
+ thermalStatus = 'Critical';
1050
+ // Parse storage
1051
+ const storageLines = storageInfo.split(/\r?\n/);
1052
+ let storageFree = 'Unknown';
1053
+ if (storageLines.length > 1) {
1054
+ const fields = storageLines[1].trim().split(/\s+/);
1055
+ if (fields.length >= 4)
1056
+ storageFree = fields[3];
1057
+ }
1058
+ const healthData = {
1059
+ batteryLevel,
1060
+ thermalStatus,
1061
+ storageFree,
1062
+ };
1063
+ if (!isBooted) {
1064
+ return Object.assign(Object.assign({}, healthData), { healthStatus: 'Unhealthy', healthCheckError: `Device boot not completed. sys.boot_completed: ${bootCompleted}` });
1065
+ }
1066
+ if (batteryLevel !== undefined && batteryLevel < 10) {
1067
+ return Object.assign(Object.assign({}, healthData), { healthStatus: 'Unhealthy', healthCheckError: `Low battery: ${batteryLevel}%` });
1068
+ }
1069
+ if (thermalStatus === 'Critical') {
1070
+ return Object.assign(Object.assign({}, healthData), { healthStatus: 'Unhealthy', healthCheckError: `Critical thermal state: ${batteryTemp}°C` });
1071
+ }
1072
+ return Object.assign(Object.assign({}, healthData), { healthStatus: 'Healthy' });
1073
+ }
1074
+ catch (err) {
1075
+ return {
1076
+ healthStatus: 'Unhealthy',
1077
+ healthCheckError: err instanceof Error ? err.message : String(err),
1078
+ };
1079
+ }
1080
+ });
1081
+ }
1082
+ recoverHealth(device) {
1083
+ return __awaiter(this, void 0, void 0, function* () {
1084
+ var _a;
1085
+ if (device.cloud)
1086
+ return true;
1087
+ try {
1088
+ logger_1.default.info(`🛡️ Attempting auto-recovery for Android device ${device.udid}...`);
1089
+ const adb = yield this.getAdbForDevice(device.udid);
1090
+ // Tier 1: Just log and hope it clears (for now)
1091
+ // Tier 2: If low battery and not charging, or critical thermal, we can't do much but alert.
1092
+ // Tier 3: If stuck booting or unresponsive, reboot.
1093
+ if (device.healthStatus === 'Unhealthy') {
1094
+ if ((_a = device.healthCheckError) === null || _a === void 0 ? void 0 : _a.includes('boot not completed')) {
1095
+ logger_1.default.info(`Device ${device.udid} is stuck booting. Attempting hard reboot...`);
1096
+ yield adb.adbExec(['-s', device.udid, 'reboot']);
1097
+ return true;
1098
+ }
1099
+ }
1100
+ return true;
1101
+ }
1102
+ catch (err) {
1103
+ logger_1.default.error(`Auto-recovery failed for ${device.udid}: ${err instanceof Error ? err.message : err}`);
1104
+ return false;
1105
+ }
1106
+ });
1107
+ }
1108
+ executeShell(udid, command) {
1109
+ return __awaiter(this, void 0, void 0, function* () {
1110
+ const ALLOWED_COMMANDS = [
1111
+ 'ls',
1112
+ 'ps',
1113
+ 'top',
1114
+ 'dumpsys battery',
1115
+ 'dumpsys wifi',
1116
+ 'dumpsys power',
1117
+ 'whoami',
1118
+ 'getprop',
1119
+ 'pm list packages',
1120
+ 'ip addr',
1121
+ 'cat /proc/meminfo',
1122
+ 'cat /proc/cpuinfo',
1123
+ 'date',
1124
+ 'uptime',
1125
+ 'netstat',
1126
+ ];
1127
+ // Basic sanitation
1128
+ const safeCommand = command.trim();
1129
+ // Check if the command starts with any allowed prefix
1130
+ const isAllowed = ALLOWED_COMMANDS.some((prefix) => safeCommand.startsWith(prefix));
1131
+ if (!isAllowed) {
1132
+ logger_1.default.warn(`Blocked potentially unsafe shell command on ${udid}: ${safeCommand}`);
1133
+ throw new Error(`Command '${safeCommand}' is not allowed for security reasons.`);
1134
+ }
1135
+ // Split command into args for adbExec
1136
+ // This is a naive split, but safe enough for the allowed commands which don't use complex quoting
1137
+ const args = safeCommand.split(/\s+/);
1138
+ logger_1.default.info(`Executing shell command on ${udid}: ${safeCommand}`);
1139
+ const { adbInstance } = yield this.getAdb();
1140
+ if (!adbInstance)
1141
+ throw new Error('ADB is not available');
1142
+ // Use device lock to ensure thread safety
1143
+ return yield DeviceLockManager_1.deviceLock.acquire(udid, () => __awaiter(this, void 0, void 0, function* () {
1144
+ // Direct raw shell execution might be better for piping, but adbExec is safer as it escapes args
1145
+ // except we passed them as array, so standard child_process rules apply.
1146
+ return yield adbInstance.adbExec(['-s', udid, 'shell', ...args], { timeout: 10000 });
1147
+ }));
1148
+ });
1149
+ }
1150
+ };
1151
+ AndroidDeviceManager = __decorate([
1152
+ (0, typedi_2.Service)(),
1153
+ __metadata("design:paramtypes", [PluginContext_1.PluginContext])
1154
+ ], AndroidDeviceManager);
1155
+ exports.default = AndroidDeviceManager;