• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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