18
18
import android .content .pm .PackageManager ;
19
19
import androidx .annotation .NonNull ;
20
20
import androidx .annotation .Nullable ;
21
+ import androidx .annotation .VisibleForTesting ;
21
22
import com .google .android .gms .common .util .AndroidUtilsLight ;
22
23
import com .google .android .gms .common .util .Hex ;
23
24
import com .google .firebase .app .distribution .Constants .ErrorMessages ;
27
28
import java .io .ByteArrayOutputStream ;
28
29
import java .io .IOException ;
29
30
import java .io .InputStream ;
30
- import java .net .MalformedURLException ;
31
- import java .net .ProtocolException ;
32
- import java .net .URL ;
33
31
import javax .net .ssl .HttpsURLConnection ;
34
32
import org .json .JSONArray ;
35
33
import org .json .JSONException ;
@@ -58,31 +56,69 @@ class FirebaseAppDistributionTesterApiClient {
58
56
59
57
public static final int DEFAULT_BUFFER_SIZE = 8192 ;
60
58
59
+ private final HttpsUrlConnectionFactory httpsUrlConnectionFactory ;
60
+
61
+ FirebaseAppDistributionTesterApiClient () {
62
+ this (new HttpsUrlConnectionFactory ());
63
+ }
64
+
65
+ @ VisibleForTesting
66
+ FirebaseAppDistributionTesterApiClient (
67
+ @ NonNull HttpsUrlConnectionFactory httpsUrlConnectionFactory ) {
68
+ this .httpsUrlConnectionFactory = httpsUrlConnectionFactory ;
69
+ }
70
+
61
71
/**
62
72
* Fetches and returns the lastest release for the app that the tester has access to, or null if
63
73
* the tester doesn't have access to any releases.
64
74
*/
65
75
@ Nullable
66
- public AppDistributionReleaseInternal fetchNewRelease (
76
+ AppDistributionReleaseInternal fetchNewRelease (
67
77
@ NonNull String fid ,
68
78
@ NonNull String appId ,
69
79
@ NonNull String apiKey ,
70
80
@ NonNull String authToken ,
71
81
@ NonNull Context context )
72
82
throws FirebaseAppDistributionException {
73
- HttpsURLConnection connection = openHttpsUrlConnection (appId , fid , apiKey , authToken , context );
83
+ HttpsURLConnection connection = null ;
84
+ int responseCode ;
74
85
String responseBody ;
75
- try (BufferedInputStream inputStream = new BufferedInputStream (connection .getInputStream ())) {
76
- responseBody = convertInputStreamToString (inputStream );
86
+ try {
87
+ connection = openHttpsUrlConnection (appId , fid , apiKey , authToken , context );
88
+ responseCode = connection .getResponseCode ();
89
+ responseBody = readResponseBody (connection );
77
90
} catch (IOException e ) {
78
- throw getExceptionForHttpResponse (connection , e );
91
+ throw new FirebaseAppDistributionException (
92
+ ErrorMessages .NETWORK_ERROR , Status .NETWORK_FAILURE , e );
79
93
} finally {
80
- connection .disconnect ();
94
+ if (connection != null ) {
95
+ connection .disconnect ();
96
+ }
81
97
}
98
+
99
+ if (!isResponseSuccess (responseCode )) {
100
+ throw getExceptionForHttpResponse (responseCode );
101
+ }
102
+
82
103
return parseNewRelease (responseBody );
83
104
}
84
105
85
- AppDistributionReleaseInternal parseNewRelease (String responseBody )
106
+ private String readResponseBody (HttpsURLConnection connection ) throws IOException {
107
+ boolean isSuccess = isResponseSuccess (connection .getResponseCode ());
108
+ InputStream inputStream = isSuccess ? connection .getInputStream () : connection .getErrorStream ();
109
+ if (inputStream == null && !isSuccess ) {
110
+ // If the server returns a response with an error code and no response body, getErrorStream
111
+ // returns null. We return an empty string to reflect the empty body.
112
+ return "" ;
113
+ }
114
+ return convertInputStreamToString (new BufferedInputStream (inputStream ));
115
+ }
116
+
117
+ private static boolean isResponseSuccess (int responseCode ) {
118
+ return responseCode >= 200 && responseCode < 300 ;
119
+ }
120
+
121
+ private AppDistributionReleaseInternal parseNewRelease (String responseBody )
86
122
throws FirebaseAppDistributionException {
87
123
try {
88
124
JSONObject responseJson = new JSONObject (responseBody );
@@ -128,40 +164,27 @@ AppDistributionReleaseInternal parseNewRelease(String responseBody)
128
164
}
129
165
}
130
166
131
- private FirebaseAppDistributionException getExceptionForHttpResponse (
132
- HttpsURLConnection connection , Exception cause ) {
133
- // TODO(lkellogg): this try-catch should be unnecessary because it will only throw an
134
- // IOException here if we couldn't connect to the server, in which case getInputStream() would
135
- // have already failed with the same exception. We also weirdly have to choose one of the two
136
- // thrown exceptions to set as the cause. We can avoid this by checking the response code
137
- // first, and then catching any unexpected exceptions when reading the input stream, essentially
138
- // combining the "default" case below with this try-catch.
139
- int responseCode ;
140
- try {
141
- responseCode = connection .getResponseCode ();
142
- } catch (IOException e ) {
143
- return new FirebaseAppDistributionException (
144
- ErrorMessages .NETWORK_ERROR , Status .NETWORK_FAILURE , e );
145
- }
146
- LogWrapper .getInstance ().e (TAG + "Failed due to " + responseCode );
167
+ private FirebaseAppDistributionException getExceptionForHttpResponse (int responseCode ) {
147
168
switch (responseCode ) {
169
+ case 400 :
170
+ return new FirebaseAppDistributionException (
171
+ "Bad request when fetching new release" , Status .UNKNOWN );
148
172
case 401 :
149
173
return new FirebaseAppDistributionException (
150
- ErrorMessages .AUTHENTICATION_ERROR , Status .AUTHENTICATION_FAILURE , cause );
174
+ ErrorMessages .AUTHENTICATION_ERROR , Status .AUTHENTICATION_FAILURE );
151
175
case 403 :
152
- case 400 :
153
176
return new FirebaseAppDistributionException (
154
- ErrorMessages .AUTHORIZATION_ERROR , Status .AUTHENTICATION_FAILURE , cause );
177
+ ErrorMessages .AUTHORIZATION_ERROR , Status .AUTHENTICATION_FAILURE );
155
178
case 404 :
156
179
return new FirebaseAppDistributionException (
157
- ErrorMessages . NOT_FOUND_ERROR , Status .AUTHENTICATION_FAILURE , cause );
180
+ "App or tester not found when fetching new release" , Status .AUTHENTICATION_FAILURE );
158
181
case 408 :
159
182
case 504 :
160
183
return new FirebaseAppDistributionException (
161
- ErrorMessages .TIMEOUT_ERROR , Status .NETWORK_FAILURE , cause );
184
+ ErrorMessages .TIMEOUT_ERROR , Status .NETWORK_FAILURE );
162
185
default :
163
186
return new FirebaseAppDistributionException (
164
- ErrorMessages . UNKNOWN_ERROR , Status .UNKNOWN , cause );
187
+ "Received error status when fetching new release: " + responseCode , Status .UNKNOWN );
165
188
}
166
189
}
167
190
@@ -173,22 +196,13 @@ private String tryGetValue(JSONObject jsonObject, String key) {
173
196
}
174
197
}
175
198
176
- HttpsURLConnection openHttpsUrlConnection (
199
+ private HttpsURLConnection openHttpsUrlConnection (
177
200
String appId , String fid , String apiKey , String authToken , Context context )
178
- throws FirebaseAppDistributionException {
201
+ throws IOException {
179
202
HttpsURLConnection httpsURLConnection ;
180
- URL url = getReleasesEndpointUrl (appId , fid );
181
- try {
182
- httpsURLConnection = (HttpsURLConnection ) url .openConnection ();
183
- } catch (IOException e ) {
184
- throw new FirebaseAppDistributionException (
185
- ErrorMessages .NETWORK_ERROR , Status .NETWORK_FAILURE , e );
186
- }
187
- try {
188
- httpsURLConnection .setRequestMethod (REQUEST_METHOD_GET );
189
- } catch (ProtocolException e ) {
190
- throw new FirebaseAppDistributionException (ErrorMessages .UNKNOWN_ERROR , Status .UNKNOWN , e );
191
- }
203
+ String url = String .format (RELEASE_ENDPOINT_URL_FORMAT , appId , fid );
204
+ httpsURLConnection = httpsUrlConnectionFactory .openConnection (url );
205
+ httpsURLConnection .setRequestMethod (REQUEST_METHOD_GET );
192
206
httpsURLConnection .setRequestProperty (API_KEY_HEADER , apiKey );
193
207
httpsURLConnection .setRequestProperty (INSTALLATION_AUTH_HEADER , authToken );
194
208
httpsURLConnection .addRequestProperty (X_ANDROID_PACKAGE_HEADER_KEY , context .getPackageName ());
@@ -197,16 +211,6 @@ HttpsURLConnection openHttpsUrlConnection(
197
211
return httpsURLConnection ;
198
212
}
199
213
200
- private URL getReleasesEndpointUrl (String appId , String fid )
201
- throws FirebaseAppDistributionException {
202
- try {
203
- return new URL (String .format (RELEASE_ENDPOINT_URL_FORMAT , appId , fid ));
204
- } catch (MalformedURLException e ) {
205
- throw new FirebaseAppDistributionException (
206
- ErrorMessages .UNKNOWN_ERROR , FirebaseAppDistributionException .Status .UNKNOWN , e );
207
- }
208
- }
209
-
210
214
private static String convertInputStreamToString (InputStream is ) throws IOException {
211
215
ByteArrayOutputStream result = new ByteArrayOutputStream ();
212
216
byte [] buffer = new byte [DEFAULT_BUFFER_SIZE ];
@@ -225,12 +229,22 @@ private String getFingerprintHashForPackage(Context context) {
225
229
hash = AndroidUtilsLight .getPackageCertificateHashBytes (context , context .getPackageName ());
226
230
227
231
if (hash == null ) {
232
+ LogWrapper .getInstance ()
233
+ .e (
234
+ TAG
235
+ + "Could not get fingerprint hash for X-Android-Cert header. Package is not signed: "
236
+ + context .getPackageName ());
228
237
return null ;
229
238
} else {
230
239
return Hex .bytesToStringUppercase (hash , /* zeroTerminated= */ false );
231
240
}
232
241
} catch (PackageManager .NameNotFoundException e ) {
233
- LogWrapper .getInstance ().e (TAG + "No such package: " + context .getPackageName (), e );
242
+ LogWrapper .getInstance ()
243
+ .e (
244
+ TAG
245
+ + "Could not get fingerprint hash for X-Android-Cert header. No such package: "
246
+ + context .getPackageName (),
247
+ e );
234
248
return null ;
235
249
}
236
250
}
0 commit comments