1/* 2 * Copyright (C) 2025 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17import {UserNotifier} from 'common/user_notifier'; 18import {ProxyTracingWarnings} from 'messaging/user_warnings'; 19import {ConnectionState} from 'trace_collection/connection_state'; 20import {TraceTarget} from 'trace_collection/trace_target'; 21import {UiTraceTarget} from 'trace_collection/ui/ui_trace_target'; 22 23export interface AdbDeviceConnectionListener { 24 onError(errorText: string): Promise<void>; 25 onConnectionStateChange(newState: ConnectionState): Promise<void>; 26 onAvailableTracesChange( 27 newTraces: UiTraceTarget[], 28 removedTraces: UiTraceTarget[], 29 ): void; 30} 31 32export abstract class AdbDeviceConnection { 33 private static readonly MULTI_DISPLAY_SCREENRECORD_VERSION = '1.4'; 34 protected state = AdbDeviceState.OFFLINE; 35 protected model = ''; 36 protected displays: string[] = []; 37 protected multiDisplayScreenRecording = false; 38 39 constructor( 40 readonly id: string, 41 protected listener: AdbDeviceConnectionListener, 42 ) {} 43 44 getState() { 45 return this.state; 46 } 47 48 hasMultiDisplayScreenRecording(): boolean { 49 return this.multiDisplayScreenRecording; 50 } 51 52 getDisplays() { 53 return this.displays; 54 } 55 56 getFormattedName(): string { 57 let status = ''; 58 if (this.state === AdbDeviceState.OFFLINE) { 59 status = 'offline'; 60 } else if (this.state === AdbDeviceState.UNAUTHORIZED) { 61 status = 'unauthorized'; 62 } 63 if (status && this.model) { 64 status += ' '; 65 } 66 return `${status}${this.model} (${this.id})`; 67 } 68 69 async checkRoot(): Promise<boolean> { 70 const root = await this.runShellCommand('su root id -u'); 71 const isRoot = Number(root) === 0; 72 if (!isRoot) { 73 UserNotifier.add( 74 new ProxyTracingWarnings([ 75 'Unable to acquire root privileges on the device - ' + 76 `check the output of 'adb -s ${this.id} shell su root id'`, 77 ]), 78 ).notify(); 79 } 80 return isRoot; 81 } 82 83 async updateAvailableTraces() { 84 if ( 85 this.state === AdbDeviceState.AVAILABLE && 86 (await this.isWaylandAvailable()) 87 ) { 88 this.listener.onAvailableTracesChange([UiTraceTarget.WAYLAND], []); 89 } else { 90 this.listener.onAvailableTracesChange([], [UiTraceTarget.WAYLAND]); 91 } 92 } 93 94 async updateProperties(resp: object) { 95 this.updatePropertiesFromResponse(resp); 96 await this.updateDisplaysInformation(); 97 } 98 99 async findFiles(path: string, matchers: string[]): Promise<string[]> { 100 if (matchers.length === 0) { 101 matchers.push(''); 102 } 103 for (const matcher of matchers) { 104 let matchingFiles: string; 105 if (matcher.length > 0) { 106 matchingFiles = await this.runShellCommand( 107 `su root find ${path} -name ${matcher}`, 108 ); 109 } else { 110 matchingFiles = await this.runShellCommand(`su root find ${path}`); 111 } 112 const files = matchingFiles 113 .split('\n') 114 .filter( 115 (file) => !file.includes('No such file') && file.trim().length > 0, 116 ); 117 if (files.length > 0) { 118 return files; 119 } 120 } 121 return []; 122 } 123 124 private async updateDisplaysInformation() { 125 let screenRecordVersion = '0'; 126 if (this.state === AdbDeviceState.AVAILABLE) { 127 try { 128 const output = await this.runShellCommand('screenrecord --version'); 129 if (!output.includes('unrecognized option')) { 130 screenRecordVersion = output; 131 } else { 132 const helpText = await this.runShellCommand('screenrecord --help'); 133 const versionStartIndex = helpText.indexOf('v') + 1; 134 screenRecordVersion = helpText.slice( 135 versionStartIndex, 136 versionStartIndex + 3, 137 ); 138 } 139 } catch (e) { 140 // swallow 141 console.error(e); 142 } 143 } 144 this.multiDisplayScreenRecording = 145 screenRecordVersion >= 146 AdbDeviceConnection.MULTI_DISPLAY_SCREENRECORD_VERSION; 147 148 if (this.state === AdbDeviceState.AVAILABLE) { 149 const output = await this.runShellCommand( 150 'su root dumpsys SurfaceFlinger --display-id', 151 ); 152 if (!output.includes('Display')) { 153 this.displays = []; 154 } else { 155 this.displays = output 156 .trim() 157 .split('\n') 158 .map((display) => { 159 const parts = display.split(' ').slice(1); 160 const displayNameStartIndex = parts.findIndex((part) => 161 part.includes('displayName'), 162 ); 163 if (displayNameStartIndex !== -1) { 164 const displayName = parts 165 .slice(displayNameStartIndex) 166 .join(' ') 167 .slice(12); 168 if (displayName.length > 2) { 169 return [displayName] 170 .concat(parts.slice(0, displayNameStartIndex)) 171 .join(' '); 172 } 173 } 174 return parts.join(' '); 175 }); 176 } 177 } else { 178 this.displays = []; 179 } 180 } 181 182 private async isWaylandAvailable(): Promise<boolean> { 183 const serviceCheck = await this.runShellCommand('service check Wayland'); 184 return !serviceCheck.includes('not found'); 185 } 186 187 abstract tryAuthorize(): Promise<void>; 188 abstract onDestroy(): void; 189 abstract runShellCommand(cmd: string): Promise<string>; 190 abstract startTrace(target: TraceTarget): Promise<void>; 191 abstract endTrace(target: TraceTarget): Promise<void>; 192 abstract pullFile(filepath: string): Promise<Uint8Array>; 193 protected abstract updatePropertiesFromResponse(resp: object): void; 194} 195 196export enum AdbDeviceState { 197 OFFLINE, 198 UNAUTHORIZED, 199 AVAILABLE, 200} 201