From 8099c9c704e8dfe45f80ac1839481aada6c4da6d Mon Sep 17 00:00:00 2001 From: Mikhail Lopatkin Date: Mon, 1 Jan 2024 22:04:23 +0100 Subject: [PATCH] Show progress dialog when initializing ADB in response to user action The dialog can cancel the action, but not the initialization of the ADB itself. Issue: #326 --- .../mlopatkin/andlogview/utils/MyFutures.java | 16 ++- .../andlogview/ui/device/AdbOpener.java | 12 ++- .../AdbServicesInitializationPresenter.java | 8 +- .../device/AdbInitProgressDialog.java | 61 ++++++++++++ .../device/MainFrameAdbInitView.java | 25 +++-- ...dbServicesInitializationPresenterTest.java | 97 +++++++++++++++++-- 6 files changed, 193 insertions(+), 26 deletions(-) create mode 100644 src/name/mlopatkin/andlogview/ui/mainframe/device/AdbInitProgressDialog.java diff --git a/base/src/main/java/name/mlopatkin/andlogview/utils/MyFutures.java b/base/src/main/java/name/mlopatkin/andlogview/utils/MyFutures.java index 4bc760df..569c6271 100644 --- a/base/src/main/java/name/mlopatkin/andlogview/utils/MyFutures.java +++ b/base/src/main/java/name/mlopatkin/andlogview/utils/MyFutures.java @@ -59,8 +59,22 @@ public static Cancellable toCancellable(CompletableFuture future) { * @return the cancellation-tolerant handler */ public static Consumer ignoreCancellations(Consumer 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 cancellationHandler(Runnable cancellationHandler, + Consumer failureHandler) { return th -> { - if (!isCancellation(th)) { + if (isCancellation(th)) { + cancellationHandler.run(); + } else { failureHandler.accept(th); } }; diff --git a/src/name/mlopatkin/andlogview/ui/device/AdbOpener.java b/src/name/mlopatkin/andlogview/ui/device/AdbOpener.java index 13311d0f..766878f3 100644 --- a/src/name/mlopatkin/andlogview/ui/device/AdbOpener.java +++ b/src/name/mlopatkin/andlogview/ui/device/AdbOpener.java @@ -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; @@ -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; } diff --git a/src/name/mlopatkin/andlogview/ui/device/AdbServicesInitializationPresenter.java b/src/name/mlopatkin/andlogview/ui/device/AdbServicesInitializationPresenter.java index f229a43e..435ce9b2 100644 --- a/src/name/mlopatkin/andlogview/ui/device/AdbServicesInitializationPresenter.java +++ b/src/name/mlopatkin/andlogview/ui/device/AdbServicesInitializationPresenter.java @@ -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. @@ -117,7 +117,7 @@ public Cancellable withAdbServicesInteractive(Consumer acti future = future.whenCompleteAsync( (services, th) -> hideProgressWithToken(token), uiExecutor); - showProgressWithToken(token); + showProgressWithToken(token, () -> result.cancel(false)); } future.handleAsync( consumingHandler(action, ignoreCancellations(adbErrorHandler()).andThen(failureHandler)), @@ -132,11 +132,11 @@ private CompletableFuture 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); } } diff --git a/src/name/mlopatkin/andlogview/ui/mainframe/device/AdbInitProgressDialog.java b/src/name/mlopatkin/andlogview/ui/mainframe/device/AdbInitProgressDialog.java new file mode 100644 index 00000000..fd4390c5 --- /dev/null +++ b/src/name/mlopatkin/andlogview/ui/mainframe/device/AdbInitProgressDialog.java @@ -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; + } +} diff --git a/src/name/mlopatkin/andlogview/ui/mainframe/device/MainFrameAdbInitView.java b/src/name/mlopatkin/andlogview/ui/mainframe/device/MainFrameAdbInitView.java index b4f21b8a..55993b02 100644 --- a/src/name/mlopatkin/andlogview/ui/mainframe/device/MainFrameAdbInitView.java +++ b/src/name/mlopatkin/andlogview/ui/mainframe/device/MainFrameAdbInitView.java @@ -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 progressDialogProvider; private final ErrorDialogs errorDialogs; + private @Nullable AdbInitProgressDialog currentDialog; + @Inject - MainFrameAdbInitView(MainFrameUi mainFrameUi, ErrorDialogs errorDialogs) { - this.mainFrameUi = mainFrameUi; + MainFrameAdbInitView(Provider 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); } } diff --git a/test/name/mlopatkin/andlogview/ui/device/AdbServicesInitializationPresenterTest.java b/test/name/mlopatkin/andlogview/ui/device/AdbServicesInitializationPresenterTest.java index 27ac0b5b..3d3a9944 100644 --- a/test/name/mlopatkin/andlogview/ui/device/AdbServicesInitializationPresenterTest.java +++ b/test/name/mlopatkin/andlogview/ui/device/AdbServicesInitializationPresenterTest.java @@ -322,7 +322,7 @@ void requestAfterCancelledStillShowsProgress(ServiceRequest request) { @ParameterizedTest @MethodSource("allInteractiveRequests") void interactiveRequestCanSucceedIfViewOpensModalDialog(ServiceRequest request) { - view.withModalLoop(() -> { + view.withModalLoop(cancellationAction -> { whenAdbInitSucceed(); thenProgressIsHidden(); @@ -336,7 +336,7 @@ void interactiveRequestCanSucceedIfViewOpensModalDialog(ServiceRequest request) @ParameterizedTest @MethodSource("allInteractiveRequests") void interactiveRequestCanFailIfViewOpensModalDialog(ServiceRequest request) { - view.withModalLoop(() -> { + view.withModalLoop(cancellationAction -> { whenAdbInitFailed(); thenProgressIsHidden(); @@ -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(); @@ -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 loop = r -> {}; @Override - public void showAdbLoadingProgress() { + public void showAdbLoadingProgress(Runnable cancellationAction) { progressAppeared = true; showsProgress = true; - loop.run(); - loop = () -> {}; + loop.accept(cancellationAction); + loop = r -> {}; } @Override @@ -601,7 +679,7 @@ public void reset() { progressAppeared = false; } - public void withModalLoop(Runnable action) { + public void withModalLoop(Consumer action) { loop = action; } } @@ -609,5 +687,6 @@ public void withModalLoop(Runnable action) { private void givenInitialState(Runnable action) { action.run(); view.reset(); + Mockito.reset(servicesConsumer, errorConsumer); } }