33
44using System ;
55using System . Collections . Generic ;
6- using System . Diagnostics ;
76using System . IO ;
87using System . Linq ;
98using System . Threading ;
@@ -16,91 +15,39 @@ namespace Xamarin.Android.Tools;
1615/// </summary>
1716public class AvdManagerRunner
1817{
19- readonly Func < string ? > getSdkPath ;
20- readonly Func < string ? > ? getJdkPath ;
18+ readonly string avdManagerPath ;
19+ readonly IDictionary < string , string > ? environmentVariables ;
2120
22- public AvdManagerRunner ( Func < string ? > getSdkPath )
23- : this ( getSdkPath , null )
24- {
25- }
26-
27- public AvdManagerRunner ( Func < string ? > getSdkPath , Func < string ? > ? getJdkPath )
28- {
29- this . getSdkPath = getSdkPath ?? throw new ArgumentNullException ( nameof ( getSdkPath ) ) ;
30- this . getJdkPath = getJdkPath ;
31- }
32-
33- public string ? AvdManagerPath {
34- get {
35- var sdkPath = getSdkPath ( ) ;
36- if ( string . IsNullOrEmpty ( sdkPath ) )
37- return null ;
38-
39- var ext = OS . IsWindows ? ".bat" : "" ;
40- var cmdlineToolsDir = Path . Combine ( sdkPath , "cmdline-tools" ) ;
41-
42- if ( Directory . Exists ( cmdlineToolsDir ) ) {
43- // Versioned dirs sorted descending, then "latest" as fallback
44- var searchDirs = Directory . GetDirectories ( cmdlineToolsDir )
45- . Select ( Path . GetFileName )
46- . Where ( n => n != "latest" && ! string . IsNullOrEmpty ( n ) )
47- . OrderByDescending ( n => Version . TryParse ( n , out var v ) ? v : new Version ( 0 , 0 ) )
48- . Append ( "latest" ) ;
49-
50- foreach ( var dir in searchDirs ) {
51- var toolPath = Path . Combine ( cmdlineToolsDir , dir ! , "bin" , "avdmanager" + ext ) ;
52- if ( File . Exists ( toolPath ) )
53- return toolPath ;
54- }
55- }
56-
57- // Legacy fallback: tools/bin/avdmanager
58- var legacyPath = Path . Combine ( sdkPath , "tools" , "bin" , "avdmanager" + ext ) ;
59- return File . Exists ( legacyPath ) ? legacyPath : null ;
60- }
61- }
62-
63- public bool IsAvailable => ! string . IsNullOrEmpty ( AvdManagerPath ) ;
64-
65- string RequireAvdManagerPath ( )
66- {
67- return AvdManagerPath ?? throw new InvalidOperationException ( "AVD Manager not found." ) ;
68- }
69-
70- void ConfigureEnvironment ( ProcessStartInfo psi )
21+ /// <summary>
22+ /// Creates a new AvdManagerRunner with the full path to the avdmanager executable.
23+ /// </summary>
24+ /// <param name="avdManagerPath">Full path to avdmanager (e.g., "/path/to/sdk/cmdline-tools/latest/bin/avdmanager").</param>
25+ /// <param name="environmentVariables">Optional environment variables to pass to avdmanager processes.</param>
26+ public AvdManagerRunner ( string avdManagerPath , IDictionary < string , string > ? environmentVariables = null )
7127 {
72- AndroidEnvironmentHelper . ConfigureEnvironment ( psi , getSdkPath ( ) , getJdkPath ? . Invoke ( ) ) ;
28+ if ( string . IsNullOrWhiteSpace ( avdManagerPath ) )
29+ throw new ArgumentException ( "Path to avdmanager must not be empty." , nameof ( avdManagerPath ) ) ;
30+ this . avdManagerPath = avdManagerPath ;
31+ this . environmentVariables = environmentVariables ;
7332 }
7433
7534 public async Task < IReadOnlyList < AvdInfo > > ListAvdsAsync ( CancellationToken cancellationToken = default )
7635 {
77- var avdManagerPath = RequireAvdManagerPath ( ) ;
78-
7936 using var stdout = new StringWriter ( ) ;
8037 using var stderr = new StringWriter ( ) ;
8138 var psi = ProcessUtils . CreateProcessStartInfo ( avdManagerPath , "list" , "avd" ) ;
82- ConfigureEnvironment ( psi ) ;
83- var exitCode = await ProcessUtils . StartProcess ( psi , stdout , stderr , cancellationToken ) . ConfigureAwait ( false ) ;
39+ var exitCode = await ProcessUtils . StartProcess ( psi , stdout , stderr , cancellationToken , environmentVariables ) . ConfigureAwait ( false ) ;
8440
85- if ( exitCode != 0 )
86- throw new InvalidOperationException ( $ "avdmanager list avd failed (exit code { exitCode } ): { stderr . ToString ( ) . Trim ( ) } ") ;
41+ ProcessUtils . ThrowIfFailed ( exitCode , "avdmanager list avd" , stderr ) ;
8742
8843 return ParseAvdListOutput ( stdout . ToString ( ) ) ;
8944 }
9045
9146 public async Task < AvdInfo > CreateAvdAsync ( string name , string systemImage , string ? deviceProfile = null ,
9247 bool force = false , CancellationToken cancellationToken = default )
9348 {
94- if ( name is null )
95- throw new ArgumentNullException ( nameof ( name ) ) ;
96- if ( name . Length == 0 )
97- throw new ArgumentException ( "Value cannot be an empty string." , nameof ( name ) ) ;
98- if ( systemImage is null )
99- throw new ArgumentNullException ( nameof ( systemImage ) ) ;
100- if ( systemImage . Length == 0 )
101- throw new ArgumentException ( "Value cannot be an empty string." , nameof ( systemImage ) ) ;
102-
103- var avdManagerPath = RequireAvdManagerPath ( ) ;
49+ ProcessUtils . ValidateNotNullOrEmpty ( name , nameof ( name ) ) ;
50+ ProcessUtils . ValidateNotNullOrEmpty ( systemImage , nameof ( systemImage ) ) ;
10451
10552 // Check if AVD already exists — return it instead of failing
10653 if ( ! force ) {
@@ -116,7 +63,7 @@ public async Task<AvdInfo> CreateAvdAsync (string name, string systemImage, stri
11663 force = true ;
11764
11865 var args = new List < string > { "create" , "avd" , "-n" , name , "-k" , systemImage } ;
119- if ( ! string . IsNullOrEmpty ( deviceProfile ) )
66+ if ( deviceProfile is { Length : > 0 } )
12067 args . AddRange ( new [ ] { "-d" , deviceProfile } ) ;
12168 if ( force )
12269 args . Add ( "--force" ) ;
@@ -125,10 +72,9 @@ public async Task<AvdInfo> CreateAvdAsync (string name, string systemImage, stri
12572 using var stderr = new StringWriter ( ) ;
12673 var psi = ProcessUtils . CreateProcessStartInfo ( avdManagerPath , args . ToArray ( ) ) ;
12774 psi . RedirectStandardInput = true ;
128- ConfigureEnvironment ( psi ) ;
12975
13076 // avdmanager prompts "Do you wish to create a custom hardware profile?" — answer "no"
131- var exitCode = await ProcessUtils . StartProcess ( psi , stdout , stderr , cancellationToken ,
77+ var exitCode = await ProcessUtils . StartProcess ( psi , stdout , stderr , cancellationToken , environmentVariables ,
13278 onStarted : p => {
13379 try {
13480 p . StandardInput . WriteLine ( "no" ) ;
@@ -138,12 +84,7 @@ public async Task<AvdInfo> CreateAvdAsync (string name, string systemImage, stri
13884 }
13985 } ) . ConfigureAwait ( false ) ;
14086
141- if ( exitCode != 0 ) {
142- var errorOutput = stderr . ToString ( ) . Trim ( ) ;
143- if ( string . IsNullOrEmpty ( errorOutput ) )
144- errorOutput = stdout . ToString ( ) . Trim ( ) ;
145- throw new InvalidOperationException ( $ "Failed to create AVD '{ name } ': { errorOutput } ") ;
146- }
87+ ProcessUtils . ThrowIfFailed ( exitCode , $ "avdmanager create avd -n { name } ", stderr , stdout ) ;
14788
14889 // Re-list to get the actual path from avdmanager (respects ANDROID_USER_HOME/ANDROID_AVD_HOME)
14990 var avds = await ListAvdsAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
@@ -161,20 +102,13 @@ public async Task<AvdInfo> CreateAvdAsync (string name, string systemImage, stri
161102
162103 public async Task DeleteAvdAsync ( string name , CancellationToken cancellationToken = default )
163104 {
164- if ( name is null )
165- throw new ArgumentNullException ( nameof ( name ) ) ;
166- if ( name . Length == 0 )
167- throw new ArgumentException ( "Value cannot be an empty string." , nameof ( name ) ) ;
168-
169- var avdManagerPath = RequireAvdManagerPath ( ) ;
105+ ProcessUtils . ValidateNotNullOrEmpty ( name , nameof ( name ) ) ;
170106
171107 using var stderr = new StringWriter ( ) ;
172108 var psi = ProcessUtils . CreateProcessStartInfo ( avdManagerPath , "delete" , "avd" , "--name" , name ) ;
173- ConfigureEnvironment ( psi ) ;
174- var exitCode = await ProcessUtils . StartProcess ( psi , null , stderr , cancellationToken ) . ConfigureAwait ( false ) ;
109+ var exitCode = await ProcessUtils . StartProcess ( psi , null , stderr , cancellationToken , environmentVariables ) . ConfigureAwait ( false ) ;
175110
176- if ( exitCode != 0 )
177- throw new InvalidOperationException ( $ "Failed to delete AVD '{ name } ': { stderr . ToString ( ) . Trim ( ) } ") ;
111+ ProcessUtils . ThrowIfFailed ( exitCode , $ "avdmanager delete avd --name { name } ", stderr ) ;
178112 }
179113
180114 internal static List < AvdInfo > ParseAvdListOutput ( string output )
@@ -208,13 +142,13 @@ internal static List<AvdInfo> ParseAvdListOutput (string output)
208142 static string GetAvdRootDirectory ( )
209143 {
210144 // ANDROID_AVD_HOME takes highest priority
211- var avdHome = Environment . GetEnvironmentVariable ( "ANDROID_AVD_HOME" ) ;
212- if ( ! string . IsNullOrEmpty ( avdHome ) )
145+ var avdHome = Environment . GetEnvironmentVariable ( EnvironmentVariableNames . AndroidAvdHome ) ;
146+ if ( avdHome is { Length : > 0 } )
213147 return avdHome ;
214148
215149 // ANDROID_USER_HOME/avd is the next option
216150 var userHome = Environment . GetEnvironmentVariable ( EnvironmentVariableNames . AndroidUserHome ) ;
217- if ( ! string . IsNullOrEmpty ( userHome ) )
151+ if ( userHome is { Length : > 0 } )
218152 return Path . Combine ( userHome , "avd" ) ;
219153
220154 // Default: ~/.android/avd
0 commit comments