Show SIM occupancy badge and add release action

- SimCardConfigurationView: render dropdown rows with a "Belegt durch <SID>"
  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) <noreply@anthropic.com>
This commit is contained in:
2026-04-27 11:03:25 +02:00
parent 9a67832faa
commit 8b313cd588
4 changed files with 192 additions and 1 deletions

View File

@@ -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<String> 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(

View File

@@ -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<String> 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<String> 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();

View File

@@ -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<SimCardInfo> simCardSelect = new ComboBox<>("SIM-Karte");
private final Paragraph statusText = new Paragraph("Lade Kuriere und SIM-Karten...");
private final Button assignButton;
private final Map<Long, String> 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<CourierInfo> couriers = simCardAssignmentService.fetchCouriers();
List<SimCardInfo> 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;
}
}

View File

@@ -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);
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