From 8b313cd5884d6790f3cf82c20224dfad02a22f4a Mon Sep 17 00:00:00 2001 From: Sven Carstensen Date: Mon, 27 Apr 2026 11:03:25 +0200 Subject: [PATCH] Show SIM occupancy badge and add release action - SimCardConfigurationView: render dropdown rows with a "Belegt durch " badge for SIM cards that have a usr_id, plus an inline unlink button that opens a confirmation dialog and releases the assignment. - SimCardAssignmentService: add releaseSimCard via DELETE /api/simcards/{id}/assign. - MainView: after install, auto-start the installed app by resolving the third-party package via "pm list packages -3" and launching it through monkey. - SecurityConfig: route unauthenticated users to SimCardConfigurationView when SSO is disabled. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../base/domain/SimCardAssignmentService.java | 15 ++++ .../base/ui/view/MainView.java | 81 +++++++++++++++++ .../ui/view/SimCardConfigurationView.java | 90 +++++++++++++++++++ .../base/ui/view/security/SecurityConfig.java | 7 +- 4 files changed, 192 insertions(+), 1 deletion(-) diff --git a/src/main/java/de/assecutor/emulatorstation/base/domain/SimCardAssignmentService.java b/src/main/java/de/assecutor/emulatorstation/base/domain/SimCardAssignmentService.java index c7893f9..00347e3 100644 --- a/src/main/java/de/assecutor/emulatorstation/base/domain/SimCardAssignmentService.java +++ b/src/main/java/de/assecutor/emulatorstation/base/domain/SimCardAssignmentService.java @@ -67,6 +67,21 @@ public class SimCardAssignmentService { } } + public void releaseSimCard(SimCardInfo simCard) { + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(buildUri("/api/simcards/" + simCard.simCardId() + "/assign")).DELETE().build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + ensureSuccessful(response, "Zuordnung der SIM-Karte konnte nicht aufgehoben werden."); + } catch (IOException e) { + throw new IllegalStateException("Zuordnung der SIM-Karte konnte nicht aufgehoben werden.", e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Aufheben der Zuordnung wurde unterbrochen.", e); + } + } + public void assignSimCard(CourierInfo courier, SimCardInfo simCard) { try { String payload = objectMapper.writeValueAsString( diff --git a/src/main/java/de/assecutor/emulatorstation/base/ui/view/MainView.java b/src/main/java/de/assecutor/emulatorstation/base/ui/view/MainView.java index 7c63842..a6785d2 100644 --- a/src/main/java/de/assecutor/emulatorstation/base/ui/view/MainView.java +++ b/src/main/java/de/assecutor/emulatorstation/base/ui/view/MainView.java @@ -40,6 +40,8 @@ import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.regex.Matcher; +import java.util.regex.Pattern; @AnonymousAllowed @Route("main") @@ -335,6 +337,8 @@ public final class MainView extends Main implements BeforeEnterObserver { downloadApp(); installApp(); + + startApp(); } } catch (Exception ex) { // Logging, Fehlerbehandlung … @@ -415,6 +419,83 @@ public final class MainView extends Main implements BeforeEnterObserver { } } + private void startApp() { + String packageName = listThirdPartyPackage(); + if (packageName == null || packageName.isBlank()) { + logger.error("Konnte Package-Namen der installierten App nicht ermitteln - App wird nicht gestartet"); + return; + } + logger.info("Starte App mit Package-Name: {}", packageName); + + HttpClient client = HttpClient.newHttpClient(); + + String jsonPayload = """ + { + "Cmd": ["adb", "shell", "monkey", "-p", "%s", "-c", "android.intent.category.LAUNCHER", "1"], + "AttachStdout": true, + "AttachStderr": true + }""".formatted(packageName); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create( + "http://" + server + ":2375/containers/android-container-" + niederlassung.name() + "/exec")) + .header("Content-Type", "application/json").POST(HttpRequest.BodyPublishers.ofString(jsonPayload)) + .build(); + + try { + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + logger.info("HTTP-Response-Code: {}", response.statusCode()); + logger.info("Response-Body: {}", response.body()); + + var execId = ExecResponse.parse(response.body()); + + exec(execId); + } catch (Exception e) { + logger.error("Fehler beim Starten der App", e); + } + } + + private String listThirdPartyPackage() { + HttpClient client = HttpClient.newHttpClient(); + + String jsonPayload = """ + { + "Cmd": ["adb", "shell", "pm", "list", "packages", "-3"], + "AttachStdout": true, + "AttachStderr": true + }"""; + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create( + "http://" + server + ":2375/containers/android-container-" + niederlassung.name() + "/exec")) + .header("Content-Type", "application/json").POST(HttpRequest.BodyPublishers.ofString(jsonPayload)) + .build(); + + try { + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + logger.info("HTTP-Response-Code: {}", response.statusCode()); + logger.info("Response-Body: {}", response.body()); + + var execId = ExecResponse.parse(response.body()); + var execResponse = exec(execId); + return extractFirstPackageName(execResponse); + } catch (Exception e) { + logger.error("Fehler beim Ermitteln des Package-Namens", e); + return null; + } + } + + private String extractFirstPackageName(String body) { + if (body == null) { + return null; + } + Matcher matcher = Pattern.compile("package:([A-Za-z0-9_.]+)").matcher(body); + if (matcher.find()) { + return matcher.group(1); + } + return null; + } + private void installApp() { HttpClient client = HttpClient.newHttpClient(); diff --git a/src/main/java/de/assecutor/emulatorstation/base/ui/view/SimCardConfigurationView.java b/src/main/java/de/assecutor/emulatorstation/base/ui/view/SimCardConfigurationView.java index 9283076..c3d7986 100644 --- a/src/main/java/de/assecutor/emulatorstation/base/ui/view/SimCardConfigurationView.java +++ b/src/main/java/de/assecutor/emulatorstation/base/ui/view/SimCardConfigurationView.java @@ -7,12 +7,14 @@ import com.vaadin.flow.component.dialog.Dialog; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.H1; import com.vaadin.flow.component.html.Paragraph; +import com.vaadin.flow.component.html.Span; import com.vaadin.flow.component.icon.Icon; import com.vaadin.flow.component.icon.VaadinIcon; import com.vaadin.flow.component.notification.Notification; import com.vaadin.flow.component.orderedlayout.FlexComponent; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.data.renderer.ComponentRenderer; import com.vaadin.flow.router.BeforeEnterEvent; import com.vaadin.flow.router.BeforeEnterObserver; import com.vaadin.flow.router.PageTitle; @@ -28,14 +30,20 @@ import de.assecutor.emulatorstation.base.ui.view.security.SsoConfiguration; import de.assecutor.emulatorstation.pojo.CourierInfo; import de.assecutor.emulatorstation.pojo.NiederlassungInfo; import de.assecutor.emulatorstation.pojo.SimCardInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.HashMap; import java.util.List; +import java.util.Map; @Route("sim-config") @PageTitle("SIM-Konfiguration") @AnonymousAllowed public class SimCardConfigurationView extends VerticalLayout implements BeforeEnterObserver { + private static final Logger logger = LoggerFactory.getLogger(SimCardConfigurationView.class); + private final AuthenticationContext authenticationContext; private final LoginSessionService loginSessionService; private final EmulatorContainerService emulatorContainerService; @@ -47,6 +55,7 @@ public class SimCardConfigurationView extends VerticalLayout implements BeforeEn private final ComboBox simCardSelect = new ComboBox<>("SIM-Karte"); private final Paragraph statusText = new Paragraph("Lade Kuriere und SIM-Karten..."); private final Button assignButton; + private final Map courierSidByUserId = new HashMap<>(); public SimCardConfigurationView(AuthenticationContext authenticationContext, LoginSessionService loginSessionService, EmulatorContainerService emulatorContainerService, @@ -89,6 +98,39 @@ public class SimCardConfigurationView extends VerticalLayout implements BeforeEn simCardSelect.setWidthFull(); simCardSelect.setPlaceholder("SIM-Karte auswählen"); simCardSelect.setItemLabelGenerator(SimCardInfo::displayLabel); + simCardSelect.setRenderer(new ComponentRenderer<>(simCard -> { + HorizontalLayout row = new HorizontalLayout(); + row.setAlignItems(Alignment.CENTER); + row.setSpacing(true); + row.setPadding(false); + row.setWidthFull(); + row.setJustifyContentMode(JustifyContentMode.BETWEEN); + + Span label = new Span(simCard.displayLabel()); + row.add(label); + + if (isSimCardOccupied(simCard)) { + String courierSid = courierSidByUserId.getOrDefault(simCard.userId(), + String.valueOf(simCard.userId())); + Span badge = new Span("Belegt durch " + courierSid); + badge.getElement().getThemeList().add("badge error small"); + + Button releaseButton = new Button(new Icon(VaadinIcon.UNLINK)); + releaseButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY_INLINE, ButtonVariant.LUMO_SMALL, + ButtonVariant.LUMO_ERROR); + releaseButton.getElement().setAttribute("title", "Zuordnung aufheben"); + releaseButton.addClickListener(event -> confirmReleaseSimCard(simCard)); + releaseButton.getElement().addEventListener("click", event -> { + }).addEventData("event.stopPropagation()"); + + HorizontalLayout actionsLayout = new HorizontalLayout(badge, releaseButton); + actionsLayout.setAlignItems(Alignment.CENTER); + actionsLayout.setSpacing(true); + actionsLayout.setPadding(false); + row.add(actionsLayout); + } + return row; + })); assignButton = new Button("Weiter", new Icon(VaadinIcon.CHECK), event -> assignSimCard()); assignButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); @@ -135,6 +177,11 @@ public class SimCardConfigurationView extends VerticalLayout implements BeforeEn List couriers = simCardAssignmentService.fetchCouriers(); List simCards = simCardAssignmentService.fetchSimCards(); + courierSidByUserId.clear(); + for (CourierInfo courier : couriers) { + courierSidByUserId.putIfAbsent(courier.userId(), courier.courierSid()); + } + courierSelect.setItems(couriers); simCardSelect.setItems(simCards); courierSelect.clear(); @@ -217,6 +264,8 @@ public class SimCardConfigurationView extends VerticalLayout implements BeforeEn } try { + logger.info("Sende SIM-Zuordnung: Kurier='{}', SIM-Karte='{}'", courier.displayLabel(), + simCard.displayLabel()); simCardAssignmentService.assignSimCard(courier, simCard); loginSessionService.markSimConfigurationCompleted(ui.getSession()); statusText.setText("SIM-Karte wurde erfolgreich zugeordnet."); @@ -233,4 +282,45 @@ public class SimCardConfigurationView extends VerticalLayout implements BeforeEn statusText.setText(message); Notification.show(message, 5000, Notification.Position.MIDDLE); } + + private void confirmReleaseSimCard(SimCardInfo simCard) { + simCardSelect.getElement().executeJs("this.close()"); + + Dialog dialog = new Dialog(); + dialog.setHeaderTitle("Zuordnung aufheben"); + + Paragraph text = new Paragraph( + "Möchten Sie die Zuordnung der SIM-Karte '" + simCard.displayLabel() + "' wirklich aufheben?"); + + Button confirmButton = new Button("Aufheben", event -> { + dialog.close(); + releaseSimCard(simCard); + }); + confirmButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_ERROR); + + Button cancelButton = new Button("Abbrechen", event -> dialog.close()); + + HorizontalLayout buttons = new HorizontalLayout(cancelButton, confirmButton); + buttons.setWidthFull(); + buttons.setJustifyContentMode(FlexComponent.JustifyContentMode.END); + + dialog.add(text, buttons); + dialog.open(); + } + + private void releaseSimCard(SimCardInfo simCard) { + try { + simCardAssignmentService.releaseSimCard(simCard); + Notification.show("Zuordnung der SIM-Karte '" + simCard.displayLabel() + "' wurde aufgehoben.", 3000, + Notification.Position.MIDDLE); + loadOptions(); + } catch (Exception ex) { + showError(ex.getMessage()); + } + } + + private static boolean isSimCardOccupied(SimCardInfo simCard) { + Long userId = simCard.userId(); + return userId != null && userId != 0L; + } } diff --git a/src/main/java/de/assecutor/emulatorstation/base/ui/view/security/SecurityConfig.java b/src/main/java/de/assecutor/emulatorstation/base/ui/view/security/SecurityConfig.java index 0b49e72..0d4494b 100644 --- a/src/main/java/de/assecutor/emulatorstation/base/ui/view/security/SecurityConfig.java +++ b/src/main/java/de/assecutor/emulatorstation/base/ui/view/security/SecurityConfig.java @@ -2,6 +2,7 @@ package de.assecutor.emulatorstation.base.ui.view.security; import com.vaadin.flow.spring.security.VaadinWebSecurity; import de.assecutor.emulatorstation.base.ui.view.LoginView; +import de.assecutor.emulatorstation.base.ui.view.SimCardConfigurationView; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; @@ -24,7 +25,11 @@ public class SecurityConfig extends VaadinWebSecurity { protected void configure(HttpSecurity http) throws Exception { super.configure(http); - setLoginView(http, LoginView.class); + if (ssoConfiguration.isEnabled()) { + setLoginView(http, LoginView.class); + } else { + setLoginView(http, SimCardConfigurationView.class); + } http.sessionManagement(session -> session // Wichtig: Session-Fixation so konfigurieren, dass Session-Attribute (z.B. "user") erhalten bleiben