Skip to content

Host activity interrupted error & dialog dismissal #3341

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Jan 30, 2022
Merged
1 change: 1 addition & 0 deletions firebase-appdistribution/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ package com.google.firebase.appdistribution {
enum_constant public static final com.google.firebase.appdistribution.FirebaseAppDistributionException.Status AUTHENTICATION_CANCELED;
enum_constant public static final com.google.firebase.appdistribution.FirebaseAppDistributionException.Status AUTHENTICATION_FAILURE;
enum_constant public static final com.google.firebase.appdistribution.FirebaseAppDistributionException.Status DOWNLOAD_FAILURE;
enum_constant public static final com.google.firebase.appdistribution.FirebaseAppDistributionException.Status HOST_ACTIVITY_INTERRUPTED;
enum_constant public static final com.google.firebase.appdistribution.FirebaseAppDistributionException.Status INSTALLATION_CANCELED;
enum_constant public static final com.google.firebase.appdistribution.FirebaseAppDistributionException.Status INSTALLATION_FAILURE;
enum_constant public static final com.google.firebase.appdistribution.FirebaseAppDistributionException.Status INSTALLATION_FAILURE_SIGNATURE_MISMATCH;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ static class ErrorMessages {

public static final String DOWNLOAD_URL_NOT_FOUND = "Download URL not found";

public static final String HOST_ACTIVITY_INTERRUPTED =
"Host activity interrupted while dialog was showing";

public static final String APK_INSTALLATION_FAILED =
"The APK failed to install or installation was cancelled";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import static com.google.firebase.appdistribution.FirebaseAppDistributionException.Status.AUTHENTICATION_CANCELED;
import static com.google.firebase.appdistribution.FirebaseAppDistributionException.Status.AUTHENTICATION_FAILURE;
import static com.google.firebase.appdistribution.FirebaseAppDistributionException.Status.HOST_ACTIVITY_INTERRUPTED;
import static com.google.firebase.appdistribution.FirebaseAppDistributionException.Status.UPDATE_NOT_AVAILABLE;
import static com.google.firebase.appdistribution.TaskUtils.safeSetTaskException;
import static com.google.firebase.appdistribution.TaskUtils.safeSetTaskResult;
Expand All @@ -34,7 +35,6 @@
import com.google.firebase.appdistribution.Constants.ErrorMessages;
import com.google.firebase.appdistribution.FirebaseAppDistributionException.Status;
import com.google.firebase.appdistribution.internal.LogWrapper;
import com.google.firebase.appdistribution.internal.SignInResultActivity;
import com.google.firebase.appdistribution.internal.SignInStorage;
import com.google.firebase.inject.Provider;
import com.google.firebase.installations.FirebaseInstallationsApi;
Expand Down Expand Up @@ -62,10 +62,15 @@ public class FirebaseAppDistribution {
private AppDistributionReleaseInternal cachedNewRelease;

private Task<AppDistributionRelease> cachedCheckForNewReleaseTask;
private AlertDialog updateDialog;
private AlertDialog updateConfirmationDialog;
private AlertDialog signInConfirmationDialog;
private boolean remakeUpdateDialog = false;
@Nullable private Activity dialogHostActivity = null;

private boolean remakeSignInConfirmationDialog = false;
private boolean remakeUpdateConfirmationDialog = false;

private TaskCompletionSource<Void> showSignInDialogTask = null;
private TaskCompletionSource<Void> showUpdateDialogTask = null;

/** Constructor for FirebaseAppDistribution */
@VisibleForTesting
Expand All @@ -85,7 +90,8 @@ public class FirebaseAppDistribution {
this.signInStorage = signInStorage;
this.lifecycleNotifier = lifecycleNotifier;
lifecycleNotifier.addOnActivityDestroyedListener(this::onActivityDestroyed);
lifecycleNotifier.addOnActivityCreatedListener(this::onActivityCreated);
lifecycleNotifier.addOnActivityPausedListener(this::onActivityPaused);
lifecycleNotifier.addOnActivityResumedListener(this::onActivityResumed);
}

/** Constructor for FirebaseAppDistribution */
Expand Down Expand Up @@ -135,17 +141,20 @@ public static FirebaseAppDistribution getInstance() {
@NonNull
public UpdateTask updateIfNewReleaseAvailable() {
synchronized (updateIfNewReleaseTaskLock) {
if (cachedUpdateIfNewReleaseTask != null && !cachedUpdateIfNewReleaseTask.isComplete()) {
if (updateIfNewReleaseAvailableIsTaskInProgress()) {
return cachedUpdateIfNewReleaseTask;
}
cachedUpdateIfNewReleaseTask = new UpdateTaskImpl();
remakeSignInConfirmationDialog = false;
remakeUpdateConfirmationDialog = false;
dialogHostActivity = null;
}

lifecycleNotifier
.applyToForegroundActivityTask(this::showSignInConfirmationDialog)
// TODO(rachelprince): Revisit this comment once changes to checkForNewRelease are reviewed
// Even though checkForNewRelease() calls signInTester(), we explicitly call signInTester
// here both for code clarifty, and because we plan to remove the signInTester() call
// here for code clarity, and because we plan to remove the signInTester() call
// from checkForNewRelease() in the near future
.onSuccessTask(unused -> signInTester())
.onSuccessTask(unused -> checkForNewRelease())
Expand All @@ -159,7 +168,7 @@ public UpdateTask updateIfNewReleaseAvailable() {
.setUpdateStatus(UpdateStatus.NEW_RELEASE_CHECK_FAILED)
.build());
}
// if the task failed, this get() will cause the error to propogate to the handler
// if the task failed, this get() will cause the error to propagate to the handler
// below
AppDistributionRelease release = task.getResult();
if (release == null) {
Expand All @@ -173,7 +182,7 @@ public UpdateTask updateIfNewReleaseAvailable() {
return Tasks.forResult(null);
}
return lifecycleNotifier.applyToForegroundActivityTask(
activity -> showUpdateAlertDialog(activity, release));
activity -> showUpdateConfirmationDialog(activity, release));
})
.onSuccessTask(
unused ->
Expand All @@ -191,34 +200,39 @@ private Task<Void> showSignInConfirmationDialog(Activity hostActivity) {
return Tasks.forResult(null);
}

TaskCompletionSource<Void> showDialogTask = new TaskCompletionSource<>();
if (showSignInDialogTask == null || showSignInDialogTask.getTask().isComplete()) {
showSignInDialogTask = new TaskCompletionSource<>();
}

signInConfirmationDialog = new AlertDialog.Builder(hostActivity).create();
dialogHostActivity = hostActivity;

Context context = firebaseApp.getApplicationContext();
signInConfirmationDialog.setTitle(context.getString(R.string.signin_dialog_title));
signInConfirmationDialog.setMessage(context.getString(R.string.singin_dialog_message));

signInConfirmationDialog.setButton(
AlertDialog.BUTTON_POSITIVE,
context.getString(R.string.singin_yes_button),
(dialogInterface, i) -> showDialogTask.setResult(null));
(dialogInterface, i) -> showSignInDialogTask.setResult(null));

signInConfirmationDialog.setButton(
AlertDialog.BUTTON_NEGATIVE,
context.getString(R.string.singin_no_button),
(dialogInterface, i) ->
showDialogTask.setException(
showSignInDialogTask.setException(
new FirebaseAppDistributionException(
ErrorMessages.AUTHENTICATION_CANCELED, AUTHENTICATION_CANCELED)));

signInConfirmationDialog.setOnCancelListener(
dialogInterface ->
showDialogTask.setException(
showSignInDialogTask.setException(
new FirebaseAppDistributionException(
ErrorMessages.AUTHENTICATION_CANCELED, AUTHENTICATION_CANCELED)));

signInConfirmationDialog.show();
return showDialogTask.getTask();

return showSignInDialogTask.getTask();
}

/** Signs in the App Distribution tester. Presents the tester with a Google sign in UI */
Expand Down Expand Up @@ -280,14 +294,14 @@ public UpdateTask updateApp() {
* basic configuration and false for advanced configuration.
*/
private UpdateTask updateApp(boolean showDownloadInNotificationManager) {
if (!isTesterSignedIn()) {
UpdateTaskImpl updateTask = new UpdateTaskImpl();
updateTask.setException(
new FirebaseAppDistributionException(
Constants.ErrorMessages.AUTHENTICATION_ERROR, AUTHENTICATION_FAILURE));
return updateTask;
}
synchronized (cachedNewReleaseLock) {
if (!isTesterSignedIn()) {
UpdateTaskImpl updateTask = new UpdateTaskImpl();
updateTask.setException(
new FirebaseAppDistributionException(
Constants.ErrorMessages.AUTHENTICATION_ERROR, AUTHENTICATION_FAILURE));
return updateTask;
}
if (cachedNewRelease == null) {
LogWrapper.getInstance().v("New release not found.");
return getErrorUpdateTask(
Expand Down Expand Up @@ -322,42 +336,48 @@ public void signOutTester() {
}

@VisibleForTesting
void onActivityDestroyed(@NonNull Activity activity) {
if (activity instanceof SignInResultActivity) {
// SignInResult is internal to the SDK and is destroyed after creation
return;
}
if (activity.isChangingConfigurations()) {
remakeSignInConfirmationDialog =
signInConfirmationDialog != null && signInConfirmationDialog.isShowing();
remakeUpdateDialog = updateDialog != null && updateDialog.isShowing();
dismissDialogs();
return;
void onActivityResumed(Activity activity) {
if (awaitingSignInDialogConfirmation()) {
if (dialogHostActivity != null && dialogHostActivity != activity) {
showSignInDialogTask.setException(
new FirebaseAppDistributionException(
ErrorMessages.HOST_ACTIVITY_INTERRUPTED, HOST_ACTIVITY_INTERRUPTED));
} else {
showSignInConfirmationDialog(activity);
}
}

if (signInConfirmationDialog != null && signInConfirmationDialog.isShowing()) {
setCachedUpdateIfNewReleaseCompletionError(
new FirebaseAppDistributionException(
ErrorMessages.AUTHENTICATION_CANCELED, AUTHENTICATION_CANCELED));
if (awaitingUpdateDialogConfirmation()) {
if (dialogHostActivity != null && dialogHostActivity != activity) {
showUpdateDialogTask.setException(
new FirebaseAppDistributionException(
ErrorMessages.HOST_ACTIVITY_INTERRUPTED, HOST_ACTIVITY_INTERRUPTED));
} else {
synchronized (cachedNewReleaseLock) {
showUpdateConfirmationDialog(
activity, ReleaseUtils.convertToAppDistributionRelease(cachedNewRelease));
}
}
}
}

if (updateDialog != null && updateDialog.isShowing()) {
setCachedUpdateIfNewReleaseCompletionError(
new FirebaseAppDistributionException(
ErrorMessages.UPDATE_CANCELED, Status.INSTALLATION_CANCELED));
@VisibleForTesting
void onActivityPaused(Activity activity) {
if (activity == dialogHostActivity) {
remakeSignInConfirmationDialog =
signInConfirmationDialog != null && signInConfirmationDialog.isShowing();
remakeUpdateConfirmationDialog =
updateConfirmationDialog != null && updateConfirmationDialog.isShowing();
dismissDialogs();
}
}

void onActivityCreated(Activity activity) {
if (remakeSignInConfirmationDialog) {
remakeSignInConfirmationDialog = false;
showSignInConfirmationDialog(activity);
} else if (remakeUpdateDialog) {
remakeUpdateDialog = false;
synchronized (cachedNewReleaseLock) {
showUpdateAlertDialog(
activity, ReleaseUtils.convertToAppDistributionRelease(cachedNewRelease));
}
@VisibleForTesting
void onActivityDestroyed(@NonNull Activity activity) {
// If the dialogHostActivity is being destroyed it is set to null. This is to ensure onResume
// shows the dialog on a configuration change and does not check the activity reference.
if (activity == dialogHostActivity) {
dialogHostActivity = null;
}
}

Expand All @@ -375,14 +395,18 @@ AppDistributionReleaseInternal getCachedNewRelease() {
}
}

private Task<Void> showUpdateAlertDialog(
private Task<Void> showUpdateConfirmationDialog(
Activity hostActivity, AppDistributionRelease newRelease) {
TaskCompletionSource<Void> showUpdateDialogTask = new TaskCompletionSource<>();

if (showUpdateDialogTask == null || showUpdateDialogTask.getTask().isComplete()) {
showUpdateDialogTask = new TaskCompletionSource<>();
}

Context context = firebaseApp.getApplicationContext();

updateDialog = new AlertDialog.Builder(hostActivity).create();
updateDialog.setTitle(context.getString(R.string.update_dialog_title));
updateConfirmationDialog = new AlertDialog.Builder(hostActivity).create();
dialogHostActivity = hostActivity;
updateConfirmationDialog.setTitle(context.getString(R.string.update_dialog_title));

StringBuilder message =
new StringBuilder(
Expand All @@ -393,28 +417,28 @@ private Task<Void> showUpdateAlertDialog(
if (newRelease.getReleaseNotes() != null && !newRelease.getReleaseNotes().isEmpty()) {
message.append(String.format("\n\nRelease notes: %s", newRelease.getReleaseNotes()));
}
updateDialog.setMessage(message);
updateConfirmationDialog.setMessage(message);

updateDialog.setButton(
updateConfirmationDialog.setButton(
AlertDialog.BUTTON_POSITIVE,
context.getString(R.string.update_yes_button),
(dialogInterface, i) -> showUpdateDialogTask.setResult(null));

updateDialog.setButton(
updateConfirmationDialog.setButton(
AlertDialog.BUTTON_NEGATIVE,
context.getString(R.string.update_no_button),
(dialogInterface, i) ->
showUpdateDialogTask.setException(
new FirebaseAppDistributionException(
ErrorMessages.UPDATE_CANCELED, Status.INSTALLATION_CANCELED)));

updateDialog.setOnCancelListener(
updateConfirmationDialog.setOnCancelListener(
dialogInterface ->
showUpdateDialogTask.setException(
new FirebaseAppDistributionException(
ErrorMessages.UPDATE_CANCELED, Status.INSTALLATION_CANCELED)));

updateDialog.show();
updateConfirmationDialog.show();

return showUpdateDialogTask.getTask();
}
Expand Down Expand Up @@ -445,8 +469,8 @@ private void dismissDialogs() {
if (signInConfirmationDialog != null && signInConfirmationDialog.isShowing()) {
signInConfirmationDialog.dismiss();
}
if (updateDialog != null && updateDialog.isShowing()) {
updateDialog.dismiss();
if (updateConfirmationDialog != null && updateConfirmationDialog.isShowing()) {
updateConfirmationDialog.dismiss();
}
}

Expand All @@ -455,4 +479,22 @@ private UpdateTaskImpl getErrorUpdateTask(Exception e) {
updateTask.setException(e);
return updateTask;
}

private boolean updateIfNewReleaseAvailableIsTaskInProgress() {
synchronized (updateIfNewReleaseTaskLock) {
return cachedUpdateIfNewReleaseTask != null && !cachedUpdateIfNewReleaseTask.isComplete();
}
}

private boolean awaitingSignInDialogConfirmation() {
return (showSignInDialogTask != null
&& !showSignInDialogTask.getTask().isComplete()
&& remakeSignInConfirmationDialog);
}

private boolean awaitingUpdateDialogConfirmation() {
return (showUpdateDialogTask != null
&& !showUpdateDialogTask.getTask().isComplete()
&& remakeUpdateConfirmationDialog);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ public enum Status {

/** Download URL for release expired */
RELEASE_URL_EXPIRED,

/** Host activity for confirmation dialog destroyed or pushed to the backstack */
HOST_ACTIVITY_INTERRUPTED,
}

@NonNull private final Status status;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,12 @@ void removeOnActivityResumedListener(@NonNull OnActivityResumedListener listener
}
}

void addOnActivityPausedListener(@NonNull OnActivityPausedListener listener) {
synchronized (lock) {
this.onActivityPausedListeners.add(listener);
}
}

@Override
public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle bundle) {
synchronized (lock) {
Expand Down
Loading