diff --git a/firebase-installations/api.txt b/firebase-installations/api.txt index b99b2cacaa9..4e676ce4849 100644 --- a/firebase-installations/api.txt +++ b/firebase-installations/api.txt @@ -72,22 +72,9 @@ package com.google.firebase.installations.remote { public class FirebaseInstallationServiceClient { ctor public FirebaseInstallationServiceClient(@NonNull Context); - method @NonNull public com.google.firebase.installations.remote.InstallationResponse createFirebaseInstallation(@NonNull String, @NonNull String, @NonNull String, @NonNull String) throws com.google.firebase.installations.remote.FirebaseInstallationServiceException; - method @NonNull public void deleteFirebaseInstallation(@NonNull String, @NonNull String, @NonNull String, @NonNull String) throws com.google.firebase.installations.remote.FirebaseInstallationServiceException; - method @NonNull public InstallationTokenResult generateAuthToken(@NonNull String, @NonNull String, @NonNull String, @NonNull String) throws com.google.firebase.installations.remote.FirebaseInstallationServiceException; - } - - public class FirebaseInstallationServiceException { - ctor public FirebaseInstallationServiceException(@NonNull com.google.firebase.installations.remote.FirebaseInstallationServiceException.Status); - ctor public FirebaseInstallationServiceException(@NonNull String, @NonNull com.google.firebase.installations.remote.FirebaseInstallationServiceException.Status); - ctor public FirebaseInstallationServiceException(@NonNull String, @NonNull com.google.firebase.installations.remote.FirebaseInstallationServiceException.Status, @NonNull Throwable); - method @NonNull public com.google.firebase.installations.remote.FirebaseInstallationServiceException.Status getStatus(); - } - - public enum FirebaseInstallationServiceException.Status { - enum_constant public static final com.google.firebase.installations.remote.FirebaseInstallationServiceException.Status NETWORK_ERROR; - enum_constant public static final com.google.firebase.installations.remote.FirebaseInstallationServiceException.Status SERVER_ERROR; - enum_constant public static final com.google.firebase.installations.remote.FirebaseInstallationServiceException.Status UNAUTHORIZED; + method @NonNull public com.google.firebase.installations.remote.InstallationResponse createFirebaseInstallation(@NonNull String, @NonNull String, @NonNull String, @NonNull String); + method @NonNull public void deleteFirebaseInstallation(@NonNull String, @NonNull String, @NonNull String, @NonNull String); + method @NonNull public InstallationTokenResult generateAuthToken(@NonNull String, @NonNull String, @NonNull String, @NonNull String); } public abstract class InstallationResponse { diff --git a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java index 594d301fac0..d64d34ffc72 100644 --- a/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java +++ b/firebase-installations/src/androidTest/java/com/google/firebase/installations/FirebaseInstallationsInstrumentedTest.java @@ -49,12 +49,12 @@ import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.Tasks; import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseException; import com.google.firebase.FirebaseOptions; import com.google.firebase.installations.local.PersistedInstallation; import com.google.firebase.installations.local.PersistedInstallation.RegistrationStatus; import com.google.firebase.installations.local.PersistedInstallationEntry; import com.google.firebase.installations.remote.FirebaseInstallationServiceClient; -import com.google.firebase.installations.remote.FirebaseInstallationServiceException; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; @@ -139,7 +139,7 @@ public class FirebaseInstallationsInstrumentedTest { .build(); @Before - public void setUp() throws FirebaseInstallationServiceException { + public void setUp() throws FirebaseException { MockitoAnnotations.initMocks(this); FirebaseApp.clearInstancesForTest(); executor = new ThreadPoolExecutor(0, 1, 30L, TimeUnit.SECONDS, new LinkedBlockingQueue<>()); @@ -168,9 +168,7 @@ public void setUp() throws FirebaseInstallationServiceException { when(backendClientReturnsError.createFirebaseInstallation( anyString(), anyString(), anyString(), anyString())) - .thenThrow( - new FirebaseInstallationServiceException( - "SDK Error", FirebaseInstallationServiceException.Status.SERVER_ERROR)); + .thenThrow(new FirebaseException("SDK Error")); when(mockUtils.createRandomFid()).thenReturn(TEST_FID_1); when(mockUtils.currentTimeInSecs()).thenReturn(TEST_CREATION_TIMESTAMP_2); @@ -180,9 +178,7 @@ public void setUp() throws FirebaseInstallationServiceException { .when(backendClientReturnsOk) .deleteFirebaseInstallation(anyString(), anyString(), anyString(), anyString()); // Mocks server error on FIS deletion - doThrow( - new FirebaseInstallationServiceException( - "Server Error", FirebaseInstallationServiceException.Status.SERVER_ERROR)) + doThrow(new FirebaseException("Server Error")) .when(backendClientReturnsError) .deleteFirebaseInstallation(anyString(), anyString(), anyString(), anyString()); } @@ -524,9 +520,7 @@ public void testGetAuthToken_serverError_failure() throws Exception { .thenReturn(REGISTERED_INSTALLATION_ENTRY); when(backendClientReturnsError.generateAuthToken( anyString(), anyString(), anyString(), anyString())) - .thenThrow( - new FirebaseInstallationServiceException( - "Server Error", FirebaseInstallationServiceException.Status.SERVER_ERROR)); + .thenThrow(new FirebaseException("Server Error")); when(mockUtils.isAuthTokenExpired(REGISTERED_INSTALLATION_ENTRY)).thenReturn(false); FirebaseInstallations firebaseInstallations = diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java index c074ba69712..709b78637a8 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/FirebaseInstallations.java @@ -23,11 +23,11 @@ import com.google.android.gms.tasks.TaskCompletionSource; import com.google.android.gms.tasks.Tasks; import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseException; import com.google.firebase.installations.local.PersistedInstallation; import com.google.firebase.installations.local.PersistedInstallation.RegistrationStatus; import com.google.firebase.installations.local.PersistedInstallationEntry; import com.google.firebase.installations.remote.FirebaseInstallationServiceClient; -import com.google.firebase.installations.remote.FirebaseInstallationServiceException; import com.google.firebase.installations.remote.InstallationResponse; import java.util.ArrayList; import java.util.Iterator; @@ -306,7 +306,7 @@ private Void registerAndSaveFid(PersistedInstallationEntry persistedInstallation .setTokenCreationEpochInSecs(creationTime) .build()); - } catch (FirebaseInstallationServiceException exception) { + } catch (FirebaseException exception) { throw new FirebaseInstallationsException( exception.getMessage(), FirebaseInstallationsException.Status.SDK_INTERNAL_ERROR); } @@ -336,7 +336,7 @@ private InstallationTokenResult fetchAuthTokenFromServer( .build()); return tokenResult; - } catch (FirebaseInstallationServiceException exception) { + } catch (FirebaseException exception) { throw new FirebaseInstallationsException( "Failed to generate auth token for a Firebase Installation.", FirebaseInstallationsException.Status.SDK_INTERNAL_ERROR); @@ -361,7 +361,7 @@ private Void deleteFirebaseInstallationId() throws FirebaseInstallationsExceptio firebaseApp.getOptions().getProjectId(), persistedInstallationEntry.getRefreshToken()); - } catch (FirebaseInstallationServiceException exception) { + } catch (FirebaseException exception) { throw new FirebaseInstallationsException( "Failed to delete a Firebase Installation.", FirebaseInstallationsException.Status.SDK_INTERNAL_ERROR); diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java index ee0723d8b74..6a1bafba6f5 100644 --- a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java +++ b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceClient.java @@ -25,7 +25,9 @@ import com.google.android.gms.common.util.AndroidUtilsLight; import com.google.android.gms.common.util.Hex; import com.google.android.gms.common.util.VisibleForTesting; +import com.google.firebase.FirebaseException; import com.google.firebase.installations.InstallationTokenResult; +import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.URL; @@ -53,10 +55,8 @@ public class FirebaseInstallationServiceClient { private static final String CONTENT_ENCODING_HEADER_KEY = "Content-Encoding"; private static final String GZIP_CONTENT_ENCODING = "gzip"; - private static final String UNAUTHORIZED_ERROR_MESSAGE = - "The request did not have the required credentials."; private static final String INTERNAL_SERVER_ERROR_MESSAGE = "There was an internal server error."; - private static final String NETWORK_ERROR_MESSAGE = "The server returned an unexpected error:"; + private static final String NETWORK_ERROR_MESSAGE = "The server returned an unexpected error: %s"; private static final String X_ANDROID_PACKAGE_HEADER_KEY = "X-Android-Package"; private static final String X_ANDROID_CERT_HEADER_KEY = "X-Android-Cert"; @@ -65,6 +65,9 @@ public class FirebaseInstallationServiceClient { private static final Pattern EXPIRATION_TIMESTAMP_PATTERN = Pattern.compile("[0-9]+s"); + private static final int MAX_RETRIES = 1; + private static final Charset UTF_8 = Charset.forName("UTF-8"); + @VisibleForTesting static final String PARSING_EXPIRATION_TIME_ERROR_MESSAGE = "Invalid Expiration Timestamp."; @@ -86,9 +89,10 @@ public FirebaseInstallationServiceClient(@NonNull Context context) { @NonNull public InstallationResponse createFirebaseInstallation( @NonNull String apiKey, @NonNull String fid, @NonNull String projectID, @NonNull String appId) - throws FirebaseInstallationServiceException { + throws FirebaseException { String resourceName = String.format(CREATE_REQUEST_RESOURCE_NAME_FORMAT, projectID); try { + int retryCount = 0; URL url = new URL( String.format( @@ -97,46 +101,36 @@ public InstallationResponse createFirebaseInstallation( FIREBASE_INSTALLATIONS_API_VERSION, resourceName, apiKey)); + while (retryCount <= MAX_RETRIES) { + HttpsURLConnection httpsURLConnection = openHttpsURLConnection(url); + httpsURLConnection.setRequestMethod("POST"); + httpsURLConnection.setDoOutput(true); - HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection(); - httpsURLConnection.setConnectTimeout(NETWORK_TIMEOUT_MILLIS); - httpsURLConnection.setReadTimeout(NETWORK_TIMEOUT_MILLIS); - httpsURLConnection.setDoOutput(true); - httpsURLConnection.setRequestMethod("POST"); - httpsURLConnection.addRequestProperty(CONTENT_TYPE_HEADER_KEY, JSON_CONTENT_TYPE); - httpsURLConnection.addRequestProperty(ACCEPT_HEADER_KEY, JSON_CONTENT_TYPE); - httpsURLConnection.addRequestProperty(CONTENT_ENCODING_HEADER_KEY, GZIP_CONTENT_ENCODING); - httpsURLConnection.addRequestProperty(X_ANDROID_PACKAGE_HEADER_KEY, context.getPackageName()); - httpsURLConnection.addRequestProperty( - X_ANDROID_CERT_HEADER_KEY, getFingerprintHashForPackage()); - - GZIPOutputStream gzipOutputStream = - new GZIPOutputStream(httpsURLConnection.getOutputStream()); - try { - gzipOutputStream.write( - buildCreateFirebaseInstallationRequestBody(fid, appId).toString().getBytes("UTF-8")); - } catch (JSONException e) { - throw new IllegalStateException(e); - } finally { - gzipOutputStream.close(); - } + GZIPOutputStream gzipOutputStream = + new GZIPOutputStream(httpsURLConnection.getOutputStream()); + try { + gzipOutputStream.write( + buildCreateFirebaseInstallationRequestBody(fid, appId).toString().getBytes("UTF-8")); + } catch (JSONException e) { + throw new IllegalStateException(e); + } finally { + gzipOutputStream.close(); + } - int httpResponseCode = httpsURLConnection.getResponseCode(); - switch (httpResponseCode) { - case 200: - return readCreateResponse(httpsURLConnection); - case 401: - throw new FirebaseInstallationServiceException( - UNAUTHORIZED_ERROR_MESSAGE, FirebaseInstallationServiceException.Status.UNAUTHORIZED); - default: - throw new FirebaseInstallationServiceException( - INTERNAL_SERVER_ERROR_MESSAGE, - FirebaseInstallationServiceException.Status.SERVER_ERROR); + int httpResponseCode = httpsURLConnection.getResponseCode(); + switch (httpResponseCode) { + case 200: + return readCreateResponse(httpsURLConnection); + case 500: + retryCount++; + break; + default: + throw new FirebaseException(readErrorResponse(httpsURLConnection)); + } } + throw new FirebaseException(INTERNAL_SERVER_ERROR_MESSAGE); } catch (IOException e) { - throw new FirebaseInstallationServiceException( - NETWORK_ERROR_MESSAGE + e.getMessage(), - FirebaseInstallationServiceException.Status.NETWORK_ERROR); + throw new FirebaseException(String.format(NETWORK_ERROR_MESSAGE, e.getMessage())); } } @@ -163,7 +157,7 @@ public void deleteFirebaseInstallation( @NonNull String fid, @NonNull String projectID, @NonNull String refreshToken) - throws FirebaseInstallationServiceException { + throws FirebaseException { String resourceName = String.format(DELETE_REQUEST_RESOURCE_NAME_FORMAT, projectID, fid); try { URL url = @@ -175,31 +169,19 @@ public void deleteFirebaseInstallation( resourceName, apiKey)); - HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection(); - httpsURLConnection.setConnectTimeout(NETWORK_TIMEOUT_MILLIS); - httpsURLConnection.setReadTimeout(NETWORK_TIMEOUT_MILLIS); - httpsURLConnection.setDoOutput(true); + HttpsURLConnection httpsURLConnection = openHttpsURLConnection(url); httpsURLConnection.setRequestMethod("DELETE"); httpsURLConnection.addRequestProperty("Authorization", "FIS_v2 " + refreshToken); - httpsURLConnection.addRequestProperty(CONTENT_TYPE_HEADER_KEY, JSON_CONTENT_TYPE); - httpsURLConnection.addRequestProperty(CONTENT_ENCODING_HEADER_KEY, GZIP_CONTENT_ENCODING); int httpResponseCode = httpsURLConnection.getResponseCode(); switch (httpResponseCode) { case 200: return; - case 401: - throw new FirebaseInstallationServiceException( - UNAUTHORIZED_ERROR_MESSAGE, FirebaseInstallationServiceException.Status.UNAUTHORIZED); default: - throw new FirebaseInstallationServiceException( - INTERNAL_SERVER_ERROR_MESSAGE, - FirebaseInstallationServiceException.Status.SERVER_ERROR); + throw new FirebaseException(readErrorResponse(httpsURLConnection)); } } catch (IOException e) { - throw new FirebaseInstallationServiceException( - NETWORK_ERROR_MESSAGE + e.getMessage(), - FirebaseInstallationServiceException.Status.NETWORK_ERROR); + throw new FirebaseException(String.format(NETWORK_ERROR_MESSAGE, e.getMessage())); } } @@ -218,10 +200,11 @@ public InstallationTokenResult generateAuthToken( @NonNull String fid, @NonNull String projectID, @NonNull String refreshToken) - throws FirebaseInstallationServiceException { + throws FirebaseException { String resourceName = String.format(GENERATE_AUTH_TOKEN_REQUEST_RESOURCE_NAME_FORMAT, projectID, fid); try { + int retryCount = 0; URL url = new URL( String.format( @@ -230,39 +213,44 @@ public InstallationTokenResult generateAuthToken( FIREBASE_INSTALLATIONS_API_VERSION, resourceName, apiKey)); + while (retryCount <= MAX_RETRIES) { + HttpsURLConnection httpsURLConnection = openHttpsURLConnection(url); + httpsURLConnection.setRequestMethod("POST"); + httpsURLConnection.addRequestProperty("Authorization", "FIS_v2 " + refreshToken); - HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection(); - httpsURLConnection.setConnectTimeout(NETWORK_TIMEOUT_MILLIS); - httpsURLConnection.setReadTimeout(NETWORK_TIMEOUT_MILLIS); - httpsURLConnection.setDoOutput(true); - httpsURLConnection.setRequestMethod("POST"); - httpsURLConnection.addRequestProperty("Authorization", "FIS_v2 " + refreshToken); - httpsURLConnection.addRequestProperty(CONTENT_TYPE_HEADER_KEY, JSON_CONTENT_TYPE); - httpsURLConnection.addRequestProperty(ACCEPT_HEADER_KEY, JSON_CONTENT_TYPE); - httpsURLConnection.addRequestProperty(CONTENT_ENCODING_HEADER_KEY, GZIP_CONTENT_ENCODING); - - int httpResponseCode = httpsURLConnection.getResponseCode(); - switch (httpResponseCode) { - case 200: - return readGenerateAuthTokenResponse(httpsURLConnection); - case 401: - throw new FirebaseInstallationServiceException( - UNAUTHORIZED_ERROR_MESSAGE, FirebaseInstallationServiceException.Status.UNAUTHORIZED); - default: - throw new FirebaseInstallationServiceException( - INTERNAL_SERVER_ERROR_MESSAGE, - FirebaseInstallationServiceException.Status.SERVER_ERROR); + int httpResponseCode = httpsURLConnection.getResponseCode(); + switch (httpResponseCode) { + case 200: + return readGenerateAuthTokenResponse(httpsURLConnection); + case 500: + retryCount++; + break; + default: + throw new FirebaseException(readErrorResponse(httpsURLConnection)); + } } + throw new FirebaseException(INTERNAL_SERVER_ERROR_MESSAGE); } catch (IOException e) { - throw new FirebaseInstallationServiceException( - NETWORK_ERROR_MESSAGE + e.getMessage(), - FirebaseInstallationServiceException.Status.NETWORK_ERROR); + throw new FirebaseException(String.format(NETWORK_ERROR_MESSAGE, e.getMessage())); } } + + private HttpsURLConnection openHttpsURLConnection(URL url) throws IOException { + HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection(); + httpsURLConnection.setConnectTimeout(NETWORK_TIMEOUT_MILLIS); + httpsURLConnection.setReadTimeout(NETWORK_TIMEOUT_MILLIS); + httpsURLConnection.addRequestProperty(CONTENT_TYPE_HEADER_KEY, JSON_CONTENT_TYPE); + httpsURLConnection.addRequestProperty(ACCEPT_HEADER_KEY, JSON_CONTENT_TYPE); + httpsURLConnection.addRequestProperty(CONTENT_ENCODING_HEADER_KEY, GZIP_CONTENT_ENCODING); + httpsURLConnection.addRequestProperty(X_ANDROID_PACKAGE_HEADER_KEY, context.getPackageName()); + httpsURLConnection.addRequestProperty( + X_ANDROID_CERT_HEADER_KEY, getFingerprintHashForPackage()); + return httpsURLConnection; + } + // Read the response from the createFirebaseInstallation API. private InstallationResponse readCreateResponse(HttpsURLConnection conn) throws IOException { - JsonReader reader = - new JsonReader(new InputStreamReader(conn.getInputStream(), Charset.defaultCharset())); + JsonReader reader = new JsonReader(new InputStreamReader(conn.getInputStream(), UTF_8)); InstallationTokenResult.Builder installationTokenResult = InstallationTokenResult.builder(); InstallationResponse.Builder builder = InstallationResponse.builder(); reader.beginObject(); @@ -301,8 +289,7 @@ private InstallationResponse readCreateResponse(HttpsURLConnection conn) throws // Read the response from the generateAuthToken FirebaseInstallation API. private InstallationTokenResult readGenerateAuthTokenResponse(HttpsURLConnection conn) throws IOException { - JsonReader reader = - new JsonReader(new InputStreamReader(conn.getInputStream(), Charset.defaultCharset())); + JsonReader reader = new JsonReader(new InputStreamReader(conn.getInputStream(), UTF_8)); InstallationTokenResult.Builder builder = InstallationTokenResult.builder(); reader.beginObject(); while (reader.hasNext()) { @@ -320,6 +307,18 @@ private InstallationTokenResult readGenerateAuthTokenResponse(HttpsURLConnection return builder.build(); } + // Read the error message from the response. + private String readErrorResponse(HttpsURLConnection conn) throws IOException { + BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getErrorStream(), UTF_8)); + StringBuilder response = new StringBuilder(); + for (String input = reader.readLine(); input != null; input = reader.readLine()) { + response.append(input).append('\n'); + } + return String.format( + "The server responded with an error. HTTP response: [%d %s %s]", + conn.getResponseCode(), conn.getResponseMessage(), response); + } + /** Gets the Android package's SHA-1 fingerprint. */ private String getFingerprintHashForPackage() { byte[] hash; diff --git a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceException.java b/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceException.java deleted file mode 100644 index b9be3727d4d..00000000000 --- a/firebase-installations/src/main/java/com/google/firebase/installations/remote/FirebaseInstallationServiceException.java +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2019 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.firebase.installations.remote; - -import androidx.annotation.NonNull; -import com.google.firebase.FirebaseException; - -/** The class for all Exceptions thrown by {@link FirebaseInstallationServiceClient}. */ -public class FirebaseInstallationServiceException extends FirebaseException { - public enum Status { - SERVER_ERROR, - - NETWORK_ERROR, - - UNAUTHORIZED - } - - @NonNull private final Status status; - - public FirebaseInstallationServiceException(@NonNull Status status) { - this.status = status; - } - - public FirebaseInstallationServiceException(@NonNull String message, @NonNull Status status) { - super(message); - this.status = status; - } - - public FirebaseInstallationServiceException( - @NonNull String message, @NonNull Status status, @NonNull Throwable cause) { - super(message, cause); - this.status = status; - } - - /** - * Gets the status status for the operation that failed. - * - * @return the status for the FirebaseInstallationServiceException - */ - @NonNull - public Status getStatus() { - return status; - } -}