Skip to content

Commit

Permalink
Show progress dialog when initializing ADB in response to user action
Browse files Browse the repository at this point in the history
The dialog can cancel the action, but not the initialization of the ADB
itself.

Issue: #326
  • Loading branch information
mlopatkin committed Jan 2, 2024
1 parent 698c4d7 commit 8099c9c
Show file tree
Hide file tree
Showing 6 changed files with 193 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,22 @@ public static Cancellable toCancellable(CompletableFuture<?> future) {
* @return the cancellation-tolerant handler
*/
public static Consumer<Throwable> ignoreCancellations(Consumer<? super Throwable> failureHandler) {
return cancellationHandler(() -> {}, failureHandler);
}

/**
* Builds a failure handler with a special handling of cancellations. It can unwrap cancellation exceptions.
*
* @param cancellationHandler the cancellation handler
* @param failureHandler the failure handler
* @return the combined handler
*/
public static Consumer<Throwable> cancellationHandler(Runnable cancellationHandler,
Consumer<? super Throwable> failureHandler) {
return th -> {
if (!isCancellation(th)) {
if (isCancellation(th)) {
cancellationHandler.run();
} else {
failureHandler.accept(th);
}
};
Expand Down
12 changes: 8 additions & 4 deletions src/name/mlopatkin/andlogview/ui/device/AdbOpener.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package name.mlopatkin.andlogview.ui.device;

import static name.mlopatkin.andlogview.utils.MyFutures.cancellationHandler;
import static name.mlopatkin.andlogview.utils.MyFutures.exceptionHandler;

import name.mlopatkin.andlogview.AppExecutors;
Expand Down Expand Up @@ -63,10 +64,13 @@ public class AdbOpener {
var result = new CompletableFuture<@Nullable AdbDataSource>();

MyFutures.cancelBy(presenter.withAdbServicesInteractive(
adb -> adbDataSourceFactory.selectDeviceAndOpenAsDataSource(
adb.getSelectDeviceDialogFactory(),
result::complete),
result::completeExceptionally), result);
adb -> adbDataSourceFactory.selectDeviceAndOpenAsDataSource(
adb.getSelectDeviceDialogFactory(),
result::complete),
cancellationHandler(
() -> result.complete(null),
result::completeExceptionally)),
result);
return result;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public interface View {
/**
* Show the user that ADB services aren't ready yet, but they are being loading.
*/
void showAdbLoadingProgress();
void showAdbLoadingProgress(Runnable cancellationAction);

/**
* Show the user that ADB services are now ready.
Expand Down Expand Up @@ -117,7 +117,7 @@ public Cancellable withAdbServicesInteractive(Consumer<? super AdbServices> acti
future = future.whenCompleteAsync(
(services, th) -> hideProgressWithToken(token),
uiExecutor);
showProgressWithToken(token);
showProgressWithToken(token, () -> result.cancel(false));
}
future.handleAsync(
consumingHandler(action, ignoreCancellations(adbErrorHandler()).andThen(failureHandler)),
Expand All @@ -132,11 +132,11 @@ private CompletableFuture<AdbServices> getServicesAsync() {
return bridge.getAdbServicesAsync();
}

private void showProgressWithToken(Object token) {
private void showProgressWithToken(Object token, Runnable cancellationAction) {
var isFirstToken = progressTokens.isEmpty();
progressTokens.add(token);
if (isFirstToken) {
view.showAdbLoadingProgress();
view.showAdbLoadingProgress(cancellationAction);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright 2024 the Andlogview authors
*
* 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 name.mlopatkin.andlogview.ui.mainframe.device;

import name.mlopatkin.andlogview.ui.mainframe.DialogFactory;
import name.mlopatkin.andlogview.utils.CommonChars;

import java.util.Objects;

import javax.inject.Inject;
import javax.swing.JDialog;
import javax.swing.JOptionPane;
import javax.swing.JProgressBar;

public class AdbInitProgressDialog {
private final JDialog dialog;
private final JOptionPane optionPane;

@Inject
public AdbInitProgressDialog(DialogFactory dialogFactory) {
optionPane = new JOptionPane(
new Object[] {
"Connecting to ADB server" + CommonChars.ELLIPSIS,
createProgressBar()
}, JOptionPane.PLAIN_MESSAGE,
JOptionPane.DEFAULT_OPTION, null, new String[] {"Cancel"});

dialog = optionPane.createDialog(dialogFactory.getOwner(), "Initializing ADB" + CommonChars.ELLIPSIS);
}

public void show(Runnable cancellationAction) {
dialog.setVisible(true);
if (!Objects.equals(optionPane.getValue(), JOptionPane.UNINITIALIZED_VALUE)) {
cancellationAction.run();
}
}

public void hide() {
dialog.setVisible(false);
}

private static JProgressBar createProgressBar() {
var bar = new JProgressBar(JProgressBar.HORIZONTAL);
bar.setIndeterminate(true);
return bar;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,38 +18,47 @@

import name.mlopatkin.andlogview.ui.device.AdbServicesInitializationPresenter;
import name.mlopatkin.andlogview.ui.mainframe.ErrorDialogs;
import name.mlopatkin.andlogview.ui.mainframe.MainFrameUi;

import java.awt.Cursor;
import org.checkerframework.checker.nullness.qual.Nullable;

import javax.inject.Inject;
import javax.inject.Provider;

/**
* A view to show ADB initialization in the Main Frame.
*/
class MainFrameAdbInitView implements AdbServicesInitializationPresenter.View {

private final MainFrameUi mainFrameUi;
private final Provider<AdbInitProgressDialog> progressDialogProvider;
private final ErrorDialogs errorDialogs;

private @Nullable AdbInitProgressDialog currentDialog;

@Inject
MainFrameAdbInitView(MainFrameUi mainFrameUi, ErrorDialogs errorDialogs) {
this.mainFrameUi = mainFrameUi;
MainFrameAdbInitView(Provider<AdbInitProgressDialog> progressDialogProvider, ErrorDialogs errorDialogs) {
this.progressDialogProvider = progressDialogProvider;
this.errorDialogs = errorDialogs;
}

@Override
public void showAdbLoadingProgress() {
mainFrameUi.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
public void showAdbLoadingProgress(Runnable cancellationAction) {
assert currentDialog == null;
currentDialog = progressDialogProvider.get();
currentDialog.show(cancellationAction);
}

@Override
public void hideAdbLoadingProgress() {
mainFrameUi.setCursor(Cursor.getDefaultCursor());
var currentDialog = this.currentDialog;
if (currentDialog != null) {
currentDialog.hide();
this.currentDialog = null;
}
}

@Override
public void showAdbLoadingError(String failureReason) {
hideAdbLoadingProgress();
errorDialogs.showAdbFailedToStartError(failureReason);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ void requestAfterCancelledStillShowsProgress(ServiceRequest request) {
@ParameterizedTest
@MethodSource("allInteractiveRequests")
void interactiveRequestCanSucceedIfViewOpensModalDialog(ServiceRequest request) {
view.withModalLoop(() -> {
view.withModalLoop(cancellationAction -> {
whenAdbInitSucceed();

thenProgressIsHidden();
Expand All @@ -336,7 +336,7 @@ void interactiveRequestCanSucceedIfViewOpensModalDialog(ServiceRequest request)
@ParameterizedTest
@MethodSource("allInteractiveRequests")
void interactiveRequestCanFailIfViewOpensModalDialog(ServiceRequest request) {
view.withModalLoop(() -> {
view.withModalLoop(cancellationAction -> {
whenAdbInitFailed();

thenProgressIsHidden();
Expand All @@ -348,15 +348,93 @@ void interactiveRequestCanFailIfViewOpensModalDialog(ServiceRequest request) {
thenErrorIsShown();
}

@ParameterizedTest
@MethodSource("allInteractiveRequests")
void interactiveRequestCanBeCancelledByDialog(ServiceRequest request) {
view.withModalLoop(cancellationAction -> {
cancellationAction.run();

thenProgressIsHidden();
});

whenRequestedAdbWith(request);

thenProgressIsHidden();
thenNoErrorIsShown();
}

@Test
void afterCancellingProgressDialogRequestCancelled() {
view.withModalLoop(cancellationAction -> {
cancellationAction.run();

thenProgressIsHidden();
});

whenRequestedAdbInteractive();

thenRequestCancelled();
}

@ParameterizedTest
@MethodSource("allInteractiveRequests")
void afterCancellingProgressNoErrorIsShown(ServiceRequest request) {
view.withModalLoop(cancellationAction -> {
cancellationAction.run();

whenAdbInitFailed();

thenProgressIsHidden();
});

whenRequestedAdbWith(request);

thenProgressIsHidden();
thenNoErrorIsShown();
}

@Test
void afterCancellingProgressDialogNewRequestCanComplete() {
givenInitialState(() -> {
view.withModalLoop(cancellationAction -> {
cancellationAction.run();

whenAdbInitSucceed();
});
whenRequestedAdbInteractive();
});

whenRequestedAdbInteractive();

thenRequestCompletedSuccessfully();
}

@Test
void afterCancellingProgressDialogNewRequestCanFail() {
givenInitialState(() -> {
view.withModalLoop(cancellationAction -> {
cancellationAction.run();

whenAdbInitFailed();
});
whenRequestedAdbInteractive();
});

whenRequestedAdbInteractive();

thenRequestFailed();
thenErrorIsShown();
}

@Test
void afterCompletingRestartWithModalDialogNewRestartShowsDialogAgain() {
givenInitialState(() -> {
view.withModalLoop(() -> whenAdbInitSucceed());
view.withModalLoop(cancellationAction -> whenAdbInitSucceed());
whenRequestedAdbWith(restartRequest());
});
withNewResultAfterReload();

view.withModalLoop(() -> whenAdbInitSucceed());
view.withModalLoop(cancellationAction -> whenAdbInitSucceed());
whenRequestedAdbWith(restartRequest());

thenProgressIsHidden();
Expand Down Expand Up @@ -551,14 +629,14 @@ private static class FakeView implements AdbServicesInitializationPresenter.View
private boolean progressAppeared;
private boolean showsProgress;
private @Nullable String showsError;
private Runnable loop = () -> {};
private Consumer<? super Runnable> loop = r -> {};

@Override
public void showAdbLoadingProgress() {
public void showAdbLoadingProgress(Runnable cancellationAction) {
progressAppeared = true;
showsProgress = true;
loop.run();
loop = () -> {};
loop.accept(cancellationAction);
loop = r -> {};
}

@Override
Expand Down Expand Up @@ -601,13 +679,14 @@ public void reset() {
progressAppeared = false;
}

public void withModalLoop(Runnable action) {
public void withModalLoop(Consumer<? super Runnable> action) {
loop = action;
}
}

private void givenInitialState(Runnable action) {
action.run();
view.reset();
Mockito.<Object>reset(servicesConsumer, errorConsumer);
}
}

0 comments on commit 8099c9c

Please sign in to comment.