{@code addr} is the sender's hotspot IP and the share port joined with a colon (the one field
+ * the ADR calls the address). The receiver composes {@code http://addr + manifestUrl} for the
+ * manifest and {@code http://addr + /v1/share//fetch/} for each item, with the token in the
+ * {@code X-Share-Token} header (never the URL). Parsing is fail-closed: an invite that does not
+ * validate returns {@code null}, mirroring {@code SyncHandshakeHelper.parsePayload}.
+ */
+public final class ShareHandshake {
+
+ private ShareHandshake() {
+ }
+
+ /** A parsed, validated Share invite: the host and port split out of {@code addr}, plus the token
+ * and manifest path. {@link #baseUrl()} is the origin the receiver prefixes to share paths. */
+ public static final class ShareInvite {
+ public final String host;
+ public final int port;
+ public final String token;
+ public final String manifestUrl;
+
+ public ShareInvite(String host, int port, String token, String manifestUrl) {
+ this.host = host;
+ this.port = port;
+ this.token = token;
+ this.manifestUrl = manifestUrl;
+ }
+
+ /** {@code http://host:port} (no trailing slash): the receiver appends the share paths. */
+ public String baseUrl() {
+ return ShareTransport.SCHEME + "://" + host + ":" + port;
+ }
+ }
+
+ /**
+ * Build the QR payload for a validated invite, or {@code null} when the inputs do not validate
+ * (so a malformed address or token is never rendered into a scannable code). {@code host} is the
+ * sender's hotspot IPv4; {@code port} is {@link ShareTransport#SHARE_PORT}.
+ */
+ public static String encode(String host, int port, String token, String manifestUrl) {
+ SyncCredentialValidator.Result check =
+ ShareInviteValidator.validate(host, port, token, manifestUrl);
+ if (!check.valid) {
+ return null;
+ }
+ try {
+ JSONObject json = new JSONObject();
+ json.put("app", ShareTransport.QR_APP_TAG);
+ json.put("addr", host + ":" + port);
+ json.put("token", token);
+ json.put("manifestUrl", manifestUrl);
+ return json.toString();
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ /**
+ * Parse a scanned payload into a validated invite, or {@code null} when it is not a Share QR or
+ * fails validation. The receiver fork calls this at the scan boundary; the scanned text is
+ * untrusted input, so every field passes {@link ShareInviteValidator} before it is returned.
+ */
+ public static ShareInvite decode(String scannedJson) {
+ try {
+ JSONObject json = new JSONObject(scannedJson);
+ if (!ShareTransport.QR_APP_TAG.equals(json.optString("app"))) {
+ return null; // not a Share invite
+ }
+ String addr = json.optString("addr", "");
+ String token = json.optString("token", "");
+ String manifestUrl = json.optString("manifestUrl", "");
+ int colon = addr.lastIndexOf(':'); // IPv4 has no colons, so the last ':' splits host:port
+ if (colon <= 0 || colon == addr.length() - 1) {
+ return null;
+ }
+ String host = addr.substring(0, colon);
+ int port = parsePort(addr.substring(colon + 1));
+ SyncCredentialValidator.Result check =
+ ShareInviteValidator.validate(host, port, token, manifestUrl);
+ if (!check.valid) {
+ return null;
+ }
+ return new ShareInvite(host, port, token, manifestUrl);
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ /** Parse the port digits, returning -1 (an invalid port) on any non-numeric or out-of-range text. */
+ private static int parsePort(String s) {
+ try {
+ return Integer.parseInt(s);
+ } catch (NumberFormatException e) {
+ return -1;
+ }
+ }
+}
diff --git a/controller/app/src/main/java/org/appdevforall/k2go/contentexchange/domain/ShareInviteValidator.java b/controller/app/src/main/java/org/appdevforall/k2go/contentexchange/domain/ShareInviteValidator.java
new file mode 100644
index 000000000..8e1ae6231
--- /dev/null
+++ b/controller/app/src/main/java/org/appdevforall/k2go/contentexchange/domain/ShareInviteValidator.java
@@ -0,0 +1,101 @@
+/*
+ * ============================================================================
+ * Name : ShareInviteValidator.java
+ * Author : AppDevForAll
+ * Copyright : Copyright (c) 2026 AppDevForAll
+ * Description : K2GO-376. Domain rule: what counts as a *safe* Share invite (the QR payload the
+ * sender shows and the receiver scans). Pure (no android.*, no JSON); the single
+ * source for the invite shape, so the sender and the receiver fork validate the
+ * same thing. Reuses the host/port rule from the sync security slice (S1).
+ * ============================================================================
+ */
+package org.appdevforall.k2go.contentexchange.domain;
+
+import org.appdevforall.k2go.sync.domain.SyncCredentialValidator;
+
+/**
+ * Pure validation of a Share invite (ADR "Pillar Share"). The invite carries the sender's reachable
+ * address and port, a session token, and the manifest path. On the sender these come from trusted
+ * local sources (the hotspot owner and the sender's own cx-core), but the RECEIVER scans the same
+ * QR as untrusted input, and both forks build against this one rule so the contract cannot drift.
+ *
+ *
The receiver later composes URLs from these fields ({@code http://host:port} + a share path),
+ * so the host/port rule is reused from {@link SyncCredentialValidator} (the hardened S1 rule) and
+ * the token/path are held to conservative, injection-free character sets.
+ */
+public final class ShareInviteValidator {
+
+ /** The cx-core token is {@code secrets.token_urlsafe(32)}: url-safe base64, ~43 chars. */
+ private static final int MAX_TOKEN_LEN = 256;
+ /** The manifest path is short ({@code /v1/share//manifest}); cap it well above that. */
+ private static final int MAX_PATH_LEN = 512;
+
+ private ShareInviteValidator() {
+ }
+
+ /** Validate every field of an invite. Call this at the build boundary (sender) and the scan
+ * boundary (receiver); reject the whole invite when it returns invalid. */
+ public static SyncCredentialValidator.Result validate(String host, int port, String token,
+ String manifestUrl) {
+ if (!SyncCredentialValidator.isValidHost(host)) {
+ return SyncCredentialValidator.Result.fail("invalid host");
+ }
+ if (!SyncCredentialValidator.isValidPort(port)) {
+ return SyncCredentialValidator.Result.fail("invalid port");
+ }
+ if (!isValidToken(token)) {
+ return SyncCredentialValidator.Result.fail("invalid token");
+ }
+ if (!isValidManifestPath(manifestUrl)) {
+ return SyncCredentialValidator.Result.fail("invalid manifest path");
+ }
+ return SyncCredentialValidator.Result.ok();
+ }
+
+ /**
+ * A token safe to place in an HTTP header and compare: a non-empty run of url-safe base64
+ * characters ({@code [A-Za-z0-9-_]}), which is exactly what {@code token_urlsafe} produces. No
+ * whitespace or control characters, so it cannot smuggle a second header line.
+ */
+ public static boolean isValidToken(String token) {
+ if (token == null || token.isEmpty() || token.length() > MAX_TOKEN_LEN) {
+ return false;
+ }
+ for (int i = 0; i < token.length(); i++) {
+ char c = token.charAt(i);
+ boolean ok = (c >= 'A' && c <= 'Z')
+ || (c >= 'a' && c <= 'z')
+ || (c >= '0' && c <= '9')
+ || c == '-'
+ || c == '_';
+ if (!ok) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * A server-relative path safe to append to {@code http://host:port}: it must start with a single
+ * {@code /}, carry no whitespace or control characters, no {@code ..} segment, and no scheme or
+ * authority ({@code ://}), so it cannot redirect the receiver to another host.
+ */
+ public static boolean isValidManifestPath(String path) {
+ if (path == null || path.length() < 2 || path.length() > MAX_PATH_LEN) {
+ return false;
+ }
+ if (path.charAt(0) != '/' || path.charAt(1) == '/') {
+ return false; // must be a single-slash absolute path, not "//host" (an authority)
+ }
+ if (path.contains("..") || path.contains("://")) {
+ return false;
+ }
+ for (int i = 0; i < path.length(); i++) {
+ char c = path.charAt(i);
+ if (c <= 0x20 || c >= 0x7f) {
+ return false; // no control characters, space or DEL
+ }
+ }
+ return true;
+ }
+}
diff --git a/controller/app/src/main/java/org/appdevforall/k2go/contentexchange/domain/ShareTransport.java b/controller/app/src/main/java/org/appdevforall/k2go/contentexchange/domain/ShareTransport.java
new file mode 100644
index 000000000..a090fd628
--- /dev/null
+++ b/controller/app/src/main/java/org/appdevforall/k2go/contentexchange/domain/ShareTransport.java
@@ -0,0 +1,40 @@
+/*
+ * ============================================================================
+ * Name : ShareTransport.java
+ * Author : AppDevForAll
+ * Copyright : Copyright (c) 2026 AppDevForAll
+ * Description : K2GO-376. One source for the content-exchange wireless-share transport
+ * constants: the off-loopback port the receiver reaches the sender on, the
+ * scheme, and the QR application tag. Pure (no android.*); the sender and the
+ * later receiver fork both read it so the two forks cannot drift apart.
+ * ============================================================================
+ */
+package org.appdevforall.k2go.contentexchange.domain;
+
+/**
+ * Transport settings for granular wireless Share (K2GO-376).
+ *
+ *
Share is the network twin of Export: the receiver PULLS from the sender's live cx-core
+ * (ADR "Pillar Share"). On the sender the share session is created over the loopback relay, but
+ * the RECEIVER reaches the gate over the hotspot at {@code http://:SHARE_PORT}.
+ * That off-loopback exposure is a separate server-side slice (plain HTTP, v1 only); this is the
+ * single constant the app points at it, so it stays in step with the exposure port.
+ */
+public final class ShareTransport {
+
+ private ShareTransport() {
+ }
+
+ /**
+ * TCP port the sender's cx-core is exposed on, off loopback, for the paired receiver. Must
+ * match the scoped-exposure slice (candidate 8095, plain HTTP for v1). The loopback relay the
+ * sender uses to CREATE the session is unrelated to this port.
+ */
+ public static final int SHARE_PORT = 8095;
+
+ /** The receiver reaches the gate over plain HTTP on the hotspot (no TLS for v1; see the ADR). */
+ public static final String SCHEME = "http";
+
+ /** QR payload {@code app} tag, so a scanned code is recognised as a Share invite (not a Sync QR). */
+ public static final String QR_APP_TAG = "iiab_share";
+}
diff --git a/controller/app/src/main/java/org/appdevforall/k2go/contentexchange/presentation/ContentManagerFragment.java b/controller/app/src/main/java/org/appdevforall/k2go/contentexchange/presentation/ContentManagerFragment.java
index 08e243223..84e4b0b1e 100644
--- a/controller/app/src/main/java/org/appdevforall/k2go/contentexchange/presentation/ContentManagerFragment.java
+++ b/controller/app/src/main/java/org/appdevforall/k2go/contentexchange/presentation/ContentManagerFragment.java
@@ -9,7 +9,8 @@
* (pick a drive via SAF, then write the open-format bundle through the
* foreground service) and the DELETE action is wired (confirm, then remove each
* selected item on the box through the foreground service, refreshing the list and
- * reclaimed space on settle); Share is a later slice and stays disabled.
+ * reclaimed space on settle). SHARE is wired too (K2GO-376): over the selection it
+ * opens a wireless share session and shows the invite QR through the share service.
* ============================================================================
*/
package org.appdevforall.k2go.contentexchange.presentation;
@@ -38,9 +39,14 @@
import org.appdevforall.k2go.contentexchange.domain.ItemRef;
import org.appdevforall.k2go.contentexchange.domain.ManagedContent;
import org.appdevforall.k2go.contentexchange.domain.StorageStatus;
+import org.appdevforall.k2go.hotspot.HotspotPermissions;
+import org.appdevforall.k2go.hotspot.LocalHotspotManager;
import org.appdevforall.k2go.storage.StorageProbe;
import org.appdevforall.k2go.ui.dialog.BrandDialog;
import org.appdevforall.k2go.util.ByteFormatter;
+import org.appdevforall.k2go.util.SnackbarDuration;
+
+import com.google.android.material.snackbar.Snackbar;
import java.util.ArrayList;
import java.util.List;
@@ -63,6 +69,13 @@ public class ContentManagerFragment extends Fragment {
private Button exportButton;
private Button importButton;
private Button deleteButton;
+ private Button shareButton;
+
+ /** The selection captured when Share was tapped, held across the hotspot-permission round-trip. */
+ private List pendingShare;
+ /** Runtime hotspot permission (NEARBY_WIFI_DEVICES / ACCESS_FINE_LOCATION), requested before the
+ * share foreground service brings the hotspot up (it cannot prompt from a service). */
+ private ActivityResultLauncher hotspotPerm;
/** Set when a delete run is launched, so the manager reloads the list (and reclaimed space) when
* it next resumes. Cleared once the reload runs and the run has settled. */
@@ -84,15 +97,20 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c
exportButton = root.findViewById(R.id.k2go_cx_export);
importButton = root.findViewById(R.id.k2go_cx_import);
deleteButton = root.findViewById(R.id.k2go_cx_delete);
+ shareButton = root.findViewById(R.id.k2go_cx_share);
openTree = registerForActivityResult(new ActivityResultContracts.OpenDocumentTree(), this::onTreePicked);
openImportTree = registerForActivityResult(
new ActivityResultContracts.OpenDocumentTree(), this::onImportTreePicked);
+ hotspotPerm = registerForActivityResult(
+ new ActivityResultContracts.RequestMultiplePermissions(), result -> onHotspotPermissionResult());
exportButton.setEnabled(true);
exportButton.setOnClickListener(v -> onExportClicked());
importButton.setOnClickListener(v -> onImportClicked());
deleteButton.setEnabled(true);
deleteButton.setOnClickListener(v -> onDeleteClicked());
+ shareButton.setEnabled(true);
+ shareButton.setOnClickListener(v -> onShareClicked());
TextView back = root.findViewById(R.id.k2go_cx_back);
back.setText(getString(R.string.k2go_back_link));
@@ -225,7 +243,7 @@ private String freeLine() {
: getString(R.string.k2go_cx_free_unavailable);
}
- /** Updates the bottom action bar from the selection. Export and Delete act; Share is pending. */
+ /** Updates the bottom action bar from the selection. Export, Share and Delete all act on it. */
private void renderSelection(SelectionSummary sel) {
// Rebind rows so group headers (tri-state) and item checkboxes match the new selection.
if (adapter != null) {
@@ -379,6 +397,64 @@ private void showProgress() {
.commit();
}
+ // ---- share ---------------------------------------------------------------------------------
+
+ /** Share: if a run is still live, just show it; otherwise capture the selection and make sure the
+ * hotspot permission is granted (the service cannot prompt) before starting. */
+ private void onShareClicked() {
+ if (ContentShareService.session().hasSession()) {
+ showShareProgress();
+ return;
+ }
+ List selected = viewModel.selectedItems();
+ if (selected.isEmpty()) {
+ return;
+ }
+ pendingShare = selected;
+ if (!LocalHotspotManager.isSupported()) {
+ String msg = getString(R.string.k2go_cx_share_hotspot_unsupported);
+ Snackbar.make(requireView(), msg, SnackbarDuration.millisForText(msg)).show();
+ return;
+ }
+ if (HotspotPermissions.granted(requireContext())) {
+ startShare(pendingShare);
+ } else {
+ hotspotPerm.launch(HotspotPermissions.required());
+ }
+ }
+
+ /** The hotspot-permission request returned: start when granted, otherwise say why we cannot. */
+ private void onHotspotPermissionResult() {
+ if (!isAdded()) {
+ return;
+ }
+ if (pendingShare != null && HotspotPermissions.granted(requireContext())) {
+ startShare(pendingShare);
+ } else {
+ pendingShare = null; // the request was declined; drop the held selection
+ String msg = getString(R.string.k2go_cx_share_need_permission);
+ Snackbar.make(requireView(), msg, SnackbarDuration.millisForText(msg)).show();
+ }
+ }
+
+ private void startShare(List selected) {
+ ItemRef[] refs = new ItemRef[selected.size()];
+ for (int i = 0; i < selected.size(); i++) {
+ refs[i] = new ItemRef(selected.get(i).app(), selected.get(i).localRef());
+ }
+ ContentShareService.start(requireContext(), refs);
+ pendingShare = null;
+ viewModel.clearSelection();
+ showShareProgress();
+ }
+
+ private void showShareProgress() {
+ requireActivity().getSupportFragmentManager().beginTransaction()
+ .replace(R.id.k2go_setup_host, new ContentShareFragment())
+ .addToBackStack(null)
+ .commit();
+ }
+
// ---- delete --------------------------------------------------------------------------------
/** Delete: if a run is still active, just show it; otherwise confirm and start.
diff --git a/controller/app/src/main/java/org/appdevforall/k2go/contentexchange/presentation/ContentShareFragment.java b/controller/app/src/main/java/org/appdevforall/k2go/contentexchange/presentation/ContentShareFragment.java
new file mode 100644
index 000000000..3e4141f81
--- /dev/null
+++ b/controller/app/src/main/java/org/appdevforall/k2go/contentexchange/presentation/ContentShareFragment.java
@@ -0,0 +1,235 @@
+/*
+ * ============================================================================
+ * Name : ContentShareFragment.java
+ * Author : AppDevForAll
+ * Copyright : Copyright (c) 2026 AppDevForAll
+ * Description : K2GO-376. Shows the Share invite: two stacked QR sections (join the hotspot, then
+ * scan to receive) once the sender foreground service has opened the share session and
+ * the hotspot is up. Reads state only (the ShareSession + the shared LocalHotspotManager
+ * state); all work is the service's. Done closes the gate + the hotspot via the service.
+ * ============================================================================
+ */
+package org.appdevforall.k2go.contentexchange.presentation;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+
+import org.appdevforall.k2go.R;
+import org.appdevforall.k2go.contentexchange.data.ShareHandshake;
+import org.appdevforall.k2go.contentexchange.domain.ShareTransport;
+import org.appdevforall.k2go.hotspot.HotspotPermissions;
+import org.appdevforall.k2go.hotspot.LocalHotspotManager;
+import org.appdevforall.k2go.redesign.QrSection;
+
+public class ContentShareFragment extends Fragment {
+
+ private TextView subtitle;
+ private TextView status;
+ private TextView waiting;
+ private View joinRoot;
+ private View receiveRoot;
+ private QrSection secJoin;
+ private QrSection secReceive;
+ private Button done;
+
+ private final ShareSession session = ContentShareService.session();
+ private final LocalHotspotManager hs = LocalHotspotManager.get();
+
+ /** One-shot guard so a re-entrant render() does not schedule the back-pop twice (K2GO-427). */
+ private boolean leaving;
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle s) {
+ View root = inflater.inflate(R.layout.fragment_k2go_content_share, container, false);
+ subtitle = root.findViewById(R.id.k2go_cxs_subtitle);
+ status = root.findViewById(R.id.k2go_cxs_status);
+ waiting = root.findViewById(R.id.k2go_cxs_waiting);
+ done = root.findViewById(R.id.k2go_cxs_done);
+
+ joinRoot = root.findViewById(R.id.k2go_cxs_join);
+ receiveRoot = root.findViewById(R.id.k2go_cxs_receive);
+ secJoin = new QrSection(joinRoot, R.id.sec_qr_frame, R.id.sec_qr, R.id.sec_qr_ph,
+ R.id.sec_caption, R.id.sec_subcaption, R.id.sec_fallback_toggle, R.id.sec_fallback, R.id.sec_fallback_values);
+ secReceive = new QrSection(receiveRoot, R.id.sec_qr_frame, R.id.sec_qr, R.id.sec_qr_ph,
+ R.id.sec_caption, R.id.sec_subcaption, R.id.sec_fallback_toggle, R.id.sec_fallback, R.id.sec_fallback_values);
+ ((TextView) joinRoot.findViewById(R.id.sec_badge)).setText("1");
+ ((TextView) joinRoot.findViewById(R.id.sec_title)).setText(R.string.k2go_cx_share_step_join);
+ ((TextView) receiveRoot.findViewById(R.id.sec_badge)).setText("2");
+ ((TextView) receiveRoot.findViewById(R.id.sec_title)).setText(R.string.k2go_cx_share_step_receive);
+
+ done.setOnClickListener(v -> {
+ ContentShareService.cancel(requireContext());
+ goBack();
+ });
+ // The AP IPv4 lands a beat after the hotspot reports ON; re-render when it (or the phase) changes
+ // so the receive QR appears the moment every piece is ready. Observed once, tied to the view.
+ hs.state().observe(getViewLifecycleOwner(), st -> render());
+ return root;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ // The token/manifest arrive from the service worker; re-render when the session state changes.
+ session.setListener(() -> {
+ if (isAdded()) {
+ render();
+ }
+ });
+ render();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ session.setListener(null);
+ }
+
+ @Override
+ public void onDestroyView() {
+ // Stop any "resolving..." ellipsis so its Handler cannot tick a destroyed view (K2GO-375).
+ if (secJoin != null) {
+ secJoin.stopPending();
+ }
+ if (secReceive != null) {
+ secReceive.stopPending();
+ }
+ leaving = false;
+ super.onDestroyView();
+ }
+
+ private void render() {
+ if (!isAdded() || secReceive == null) {
+ return;
+ }
+ // A cleared / absent session means the share was stopped: leave this screen.
+ if (!session.hasSession()) {
+ goBack();
+ return;
+ }
+ subtitle.setText(getString(R.string.k2go_cx_share_subtitle_fmt, session.itemCount()));
+
+ if (session.hasError()) {
+ showStatus(session.errorMessage() != null
+ ? session.errorMessage() : getString(R.string.k2go_cx_share_error));
+ return;
+ }
+ if (session.isPreparing()) {
+ showStatus(getString(R.string.k2go_cx_share_preparing));
+ return;
+ }
+ // READY: the gate exists; the QR needs the hotspot up and its address resolved.
+ renderInvite();
+ }
+
+ private void renderInvite() {
+ LocalHotspotManager.State st = hs.state().getValue();
+ LocalHotspotManager.Phase phase = st != null ? st.phase : LocalHotspotManager.Phase.OFF;
+
+ if (!LocalHotspotManager.isSupported()) {
+ showStatus(getString(R.string.k2go_cx_share_hotspot_unsupported));
+ return;
+ }
+ if (phase == LocalHotspotManager.Phase.FAILED
+ || (phase != LocalHotspotManager.Phase.ON && !HotspotPermissions.granted(requireContext()))) {
+ showStatus(getString(R.string.k2go_cx_share_hotspot_failed));
+ return;
+ }
+
+ // OFF / STARTING / ON: show the two sections; only ON has scannable codes.
+ showSections();
+ boolean on = phase == LocalHotspotManager.Phase.ON;
+ if (!on) {
+ String starting = getString(R.string.k2go_cx_share_starting_hotspot);
+ for (QrSection sec : new QrSection[]{secJoin, secReceive}) {
+ sec.setQrPending(requireContext(), starting);
+ sec.caption.setText("");
+ sec.subCaption.setText("");
+ sec.setFallback(requireContext(), null);
+ }
+ waiting.setVisibility(View.GONE);
+ return;
+ }
+
+ String ssid = st != null && st.ssid != null ? st.ssid : "";
+ String pass = st != null && st.passphrase != null ? st.passphrase : "";
+ String ip = st != null ? st.hotspotIp : null;
+
+ // Section 1: join the hotspot (standard Wi-Fi auto-join QR + the credentials as fallback).
+ secJoin.setQr(requireContext(), "WIFI:S:" + ssid + ";T:WPA;P:" + pass + ";;", null);
+ secJoin.caption.setText(R.string.k2go_cx_share_join_caption);
+ secJoin.subCaption.setText("");
+ secJoin.setFallback(requireContext(), new String[]{
+ getString(R.string.k2go_fallback_wifi, ssid), getString(R.string.k2go_fallback_pass, pass)});
+
+ // Section 2: the share invite QR. Needs the AP IPv4, which resolves a beat after ON.
+ secReceive.caption.setText(R.string.k2go_cx_share_receive_caption);
+ secReceive.subCaption.setText("");
+ String invite = ip != null
+ ? ShareHandshake.encode(ip, ShareTransport.SHARE_PORT, session.token(), session.manifestUrl())
+ : null;
+ if (invite == null) {
+ // No dead QR at a guessed address; hold the slot until the real AP IP arrives (render reruns).
+ secReceive.setQrPending(requireContext(), getString(R.string.k2go_cx_share_resolving_address));
+ secReceive.setFallback(requireContext(), null);
+ waiting.setVisibility(View.GONE);
+ } else {
+ secReceive.setQr(requireContext(), invite, null);
+ secReceive.setFallback(requireContext(), null);
+ waiting.setVisibility(View.VISIBLE);
+ }
+ }
+
+ /** Show a single status line (preparing / hotspot / error) instead of the two QR cards. */
+ private void showStatus(String text) {
+ status.setText(text);
+ status.setVisibility(View.VISIBLE);
+ secJoin.stopPending();
+ secReceive.stopPending();
+ joinRoot.setVisibility(View.GONE);
+ receiveRoot.setVisibility(View.GONE);
+ waiting.setVisibility(View.GONE);
+ }
+
+ private void showSections() {
+ status.setVisibility(View.GONE);
+ joinRoot.setVisibility(View.VISIBLE);
+ receiveRoot.setVisibility(View.VISIBLE);
+ }
+
+ /**
+ * Leave this screen. Mirrors ExportProgressFragment: the pop is deferred to the next main-loop tick
+ * and re-checked there, so a render() driven from inside a FragmentManager transaction (the listener,
+ * or the service beginning the session on a later message) never calls popBackStack mid-transaction
+ * ("FragmentManager is already executing transactions", K2GO-427).
+ */
+ private void goBack() {
+ if (leaving) {
+ return;
+ }
+ View v = getView();
+ if (v == null) {
+ return;
+ }
+ leaving = true;
+ v.post(() -> {
+ if (!isAdded()) {
+ return;
+ }
+ if (session.hasSession()) {
+ leaving = false; // a run exists now: stay, and let a later leave (Done) reschedule
+ return;
+ }
+ getParentFragmentManager().popBackStack();
+ });
+ }
+}
diff --git a/controller/app/src/main/java/org/appdevforall/k2go/contentexchange/presentation/ContentShareService.java b/controller/app/src/main/java/org/appdevforall/k2go/contentexchange/presentation/ContentShareService.java
new file mode 100644
index 000000000..11baee4d7
--- /dev/null
+++ b/controller/app/src/main/java/org/appdevforall/k2go/contentexchange/presentation/ContentShareService.java
@@ -0,0 +1,292 @@
+/*
+ * ============================================================================
+ * Name : ContentShareService.java
+ * Author : AppDevForAll
+ * Copyright : Copyright (c) 2026 AppDevForAll
+ * Description : K2GO-376. Sender foreground service for granular wireless Share. Over the current
+ * content-manager selection it opens a scoped share session on the LOCAL cx-core
+ * (loopback), brings the hotspot up (only if it was off), holds both the hotspot and
+ * the gate open with CPU + Wi-Fi locks, and on teardown closes the gate (DELETE) and
+ * stops the hotspot only if it started it. It copies NO bytes: cx-core streams to the
+ * receiver that pulls over the hotspot. The invite/QR fragment observes SESSION.
+ * ============================================================================
+ */
+package org.appdevforall.k2go.contentexchange.presentation;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.net.wifi.WifiManager;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.PowerManager;
+
+import androidx.annotation.Nullable;
+import androidx.core.app.NotificationCompat;
+import androidx.core.content.ContextCompat;
+
+import org.appdevforall.k2go.R;
+import org.appdevforall.k2go.contentexchange.data.CxCoreDataSource;
+import org.appdevforall.k2go.contentexchange.domain.ItemRef;
+import org.appdevforall.k2go.hotspot.HotspotPermissions;
+import org.appdevforall.k2go.hotspot.LocalHotspotManager;
+import org.appdevforall.k2go.notifications.NotificationIds;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public final class ContentShareService extends Service {
+
+ public static final String ACTION_START = "org.appdevforall.k2go.CONTENT_SHARE_START";
+ public static final String ACTION_CANCEL = "org.appdevforall.k2go.CONTENT_SHARE_CANCEL";
+
+ public static final String EXTRA_APPS = "apps";
+ public static final String EXTRA_LOCAL_REFS = "localRefs";
+
+ private static final String CHANNEL_ID = "content_share_channel";
+ private static final int NOTIFICATION_ID = NotificationIds.CONTENT_SHARE;
+
+ /** The single run state the invite/QR fragment observes. */
+ private static final ShareSession SESSION = new ShareSession();
+
+ static ShareSession session() {
+ return SESSION;
+ }
+
+ private final ExecutorService worker = Executors.newSingleThreadExecutor();
+
+ // The open session on the local cx-core, held so onDestroy can close the gate. Set by the worker.
+ private volatile String sessionId;
+ private volatile String token;
+ // True only when THIS service started the hotspot, so teardown never yanks a hotspot that Connect
+ // or Clone brought up (LocalHotspotManager is a shared, process-bound singleton).
+ private volatile boolean ownedHotspot;
+
+ private PowerManager.WakeLock wakeLock;
+ private WifiManager.WifiLock wifiLock;
+
+ // ---- static control surface (the fragment calls these) -------------------------------------
+
+ public static void start(Context ctx, ItemRef[] refs) {
+ String[] apps = new String[refs.length];
+ String[] locals = new String[refs.length];
+ for (int i = 0; i < refs.length; i++) {
+ apps[i] = refs[i].app();
+ locals[i] = refs[i].localRef();
+ }
+ Intent i = new Intent(ctx, ContentShareService.class).setAction(ACTION_START)
+ .putExtra(EXTRA_APPS, apps)
+ .putExtra(EXTRA_LOCAL_REFS, locals);
+ ContextCompat.startForegroundService(ctx, i);
+ }
+
+ /** Stop sharing (the user's "Done"/"Stop", and the notification action): closes the gate and the
+ * hotspot in onDestroy. */
+ public static void cancel(Context ctx) {
+ ContextCompat.startForegroundService(ctx,
+ new Intent(ctx, ContentShareService.class).setAction(ACTION_CANCEL));
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ createChannel();
+ }
+
+ @Nullable
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ String action = intent != null ? intent.getAction() : null;
+
+ if (ACTION_CANCEL.equals(action)) {
+ // Satisfy the FGS start-foreground obligation (a fresh instance may handle this), then tear
+ // down. onDestroy closes the gate and stops the hotspot.
+ startForeground(NOTIFICATION_ID, buildNotification());
+ SESSION.clear();
+ stopForeground(true);
+ stopSelf();
+ return START_NOT_STICKY;
+ }
+
+ // A sticky restart after a process kill delivers a null intent. The hotspot is process-bound, so
+ // it died with the process and there is nothing to resume; do not re-hold locks for a dead share.
+ if (intent == null && !SESSION.hasSession()) {
+ stopSelf();
+ return START_NOT_STICKY;
+ }
+
+ if (SESSION.hasSession()) {
+ // A duplicate start while a share is live. This onStartCommand may come from
+ // startForegroundService, so honor the FGS start obligation before ignoring it; the
+ // fragment shows the running share.
+ startForeground(NOTIFICATION_ID, buildNotification());
+ return START_STICKY;
+ }
+
+ // START: a fresh share over the intent's selection.
+ final ItemRef[] refs = readRefs(intent);
+ if (refs.length == 0) {
+ stopSelf();
+ return START_NOT_STICKY;
+ }
+ SESSION.beginPreparing(refs.length);
+ startForeground(NOTIFICATION_ID, buildNotification());
+ acquireHardwareLocks();
+ ensureHotspot();
+ worker.execute(() -> openSession(refs));
+ // START_STICKY (like CloneShareService): keep the process at foreground priority so the
+ // process-bound hotspot and the live stream survive backgrounding and Doze.
+ return START_STICKY;
+ }
+
+ // ---- the run -------------------------------------------------------------------------------
+
+ /** Open the scoped share session on the local cx-core and publish the invite. Copies no bytes. */
+ private void openSession(ItemRef[] refs) {
+ List items = new ArrayList<>();
+ for (ItemRef ref : refs) {
+ items.add(ref);
+ }
+ try {
+ CxCoreDataSource.ShareCreated created = new CxCoreDataSource().postShareCreate(items);
+ if (created.sessionId.isEmpty() || created.token.isEmpty() || created.manifestUrl.isEmpty()) {
+ SESSION.fail(getString(R.string.k2go_cx_share_error));
+ return;
+ }
+ // Store before publishing, so a teardown that races the publish still has the id to close.
+ this.sessionId = created.sessionId;
+ this.token = created.token;
+ SESSION.setReady(created.sessionId, created.token, created.manifestUrl);
+ } catch (Exception e) {
+ SESSION.fail(getString(R.string.k2go_cx_share_error));
+ }
+ }
+
+ /**
+ * Bring the hotspot up only if it is currently off. If Connect or Clone already has it up (ON or
+ * STARTING), this service does not own it and must not stop it on teardown. The runtime hotspot
+ * permission is gated by the fragment before start; without it the hotspot stays off and the
+ * fragment surfaces the failure from LocalHotspotManager's state.
+ */
+ private void ensureHotspot() {
+ if (!LocalHotspotManager.isSupported()) {
+ return;
+ }
+ LocalHotspotManager hs = LocalHotspotManager.get();
+ LocalHotspotManager.State st = hs.state().getValue();
+ LocalHotspotManager.Phase phase = st != null ? st.phase : LocalHotspotManager.Phase.OFF;
+ boolean up = phase == LocalHotspotManager.Phase.ON || phase == LocalHotspotManager.Phase.STARTING;
+ if (!up && HotspotPermissions.granted(this)) {
+ ownedHotspot = true;
+ hs.start(getApplicationContext());
+ }
+ }
+
+ private void acquireHardwareLocks() {
+ PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
+ if (pm != null && wakeLock == null) {
+ wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "IIAB:ContentShareWakeLock");
+ wakeLock.acquire();
+ }
+ WifiManager wm = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
+ if (wm != null && wifiLock == null) {
+ wifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "IIAB:ContentShareWifiLock");
+ wifiLock.acquire();
+ }
+ }
+
+ private void releaseHardwareLocks() {
+ if (wakeLock != null && wakeLock.isHeld()) {
+ wakeLock.release();
+ }
+ wakeLock = null;
+ if (wifiLock != null && wifiLock.isHeld()) {
+ wifiLock.release();
+ }
+ wifiLock = null;
+ }
+
+ @Override
+ public void onDestroy() {
+ releaseHardwareLocks();
+ // Stop the hotspot only if this service started it (shared singleton: never yank Connect's).
+ if (ownedHotspot) {
+ LocalHotspotManager.get().stop();
+ ownedHotspot = false;
+ }
+ // Close the gate (the app-side closer, ADR "Pillar Share"). Off the main thread; the process
+ // usually outlives this service (the app is still open). A rare race where the id is not stored
+ // yet, or a process kill, is covered by cx-core's safety cap and the receiver's complete.
+ final String id = sessionId;
+ final String tok = token;
+ if (id != null && tok != null) {
+ new Thread(() -> {
+ try {
+ new CxCoreDataSource().deleteShare(id, tok);
+ } catch (Exception ignored) {
+ // best-effort close; cx-core's safety cap reclaims an abandoned session
+ }
+ }, "content-share-close").start();
+ }
+ worker.shutdownNow();
+ super.onDestroy();
+ }
+
+ // ---- helpers -------------------------------------------------------------------------------
+
+ private static ItemRef[] readRefs(Intent intent) {
+ if (intent == null) {
+ return new ItemRef[0];
+ }
+ String[] apps = intent.getStringArrayExtra(EXTRA_APPS);
+ String[] locals = intent.getStringArrayExtra(EXTRA_LOCAL_REFS);
+ if (apps == null || locals == null || apps.length != locals.length) {
+ return new ItemRef[0];
+ }
+ ItemRef[] refs = new ItemRef[apps.length];
+ for (int i = 0; i < apps.length; i++) {
+ refs[i] = new ItemRef(apps[i], locals[i]);
+ }
+ return refs;
+ }
+
+ // ---- notification --------------------------------------------------------------------------
+
+ private Notification buildNotification() {
+ Intent cancel = new Intent(this, ContentShareService.class).setAction(ACTION_CANCEL);
+ PendingIntent cancelIntent = PendingIntent.getService(this, 1, cancel,
+ PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
+ return new NotificationCompat.Builder(this, CHANNEL_ID)
+ .setContentTitle(getString(R.string.k2go_cx_share_notif_title))
+ .setContentText(getString(R.string.k2go_cx_share_notif_text))
+ .setSmallIcon(android.R.drawable.stat_sys_upload)
+ .setOngoing(true)
+ .setPriority(NotificationCompat.PRIORITY_LOW)
+ .setOnlyAlertOnce(true)
+ .addAction(0, getString(R.string.k2go_cx_share_stop), cancelIntent)
+ .build();
+ }
+
+ private void createChannel() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ NotificationChannel channel = new NotificationChannel(
+ CHANNEL_ID, getString(R.string.k2go_cx_share_channel_name),
+ NotificationManager.IMPORTANCE_LOW);
+ NotificationManager manager = getSystemService(NotificationManager.class);
+ if (manager != null) {
+ manager.createNotificationChannel(channel);
+ }
+ }
+ }
+}
diff --git a/controller/app/src/main/java/org/appdevforall/k2go/contentexchange/presentation/ShareSession.java b/controller/app/src/main/java/org/appdevforall/k2go/contentexchange/presentation/ShareSession.java
new file mode 100644
index 000000000..d4b62ad43
--- /dev/null
+++ b/controller/app/src/main/java/org/appdevforall/k2go/contentexchange/presentation/ShareSession.java
@@ -0,0 +1,128 @@
+/*
+ * ============================================================================
+ * Name : ShareSession.java
+ * Author : AppDevForAll
+ * Copyright : Copyright (c) 2026 AppDevForAll
+ * Description : K2GO-376. The one share-run state the invite/QR fragment observes and the sender
+ * foreground service mutates. Encapsulated (no public mutable fields), mirroring
+ * ExportSession: the service owns the single instance and the fragment reads it
+ * through getters. Holds the invite (session id + token + manifest path) the QR needs.
+ * ============================================================================
+ */
+package org.appdevforall.k2go.contentexchange.presentation;
+
+import android.os.Handler;
+import android.os.Looper;
+
+final class ShareSession {
+
+ /** Pinged on the main thread whenever the run state changes. */
+ interface Listener {
+ void onUpdate();
+ }
+
+ /** PREPARING: creating the session on the local cx-core. READY: the invite exists; show the QR. */
+ enum Phase { PREPARING, READY, ERROR }
+
+ private final Handler main = new Handler(Looper.getMainLooper());
+
+ private Listener listener;
+
+ private volatile boolean active = false; // a share run exists (set until clear)
+ private volatile Phase phase = Phase.PREPARING;
+ private int itemCount;
+ private String sessionId;
+ private String token;
+ private String manifestUrl;
+ private String errorMessage;
+
+ void setListener(Listener l) {
+ this.listener = l;
+ }
+
+ // ---- service-side mutation -----------------------------------------------------------------
+
+ /** Start a run: the item count is fixed now; the invite arrives once cx-core opens the session. */
+ void beginPreparing(int itemCount) {
+ this.itemCount = itemCount;
+ this.sessionId = null;
+ this.token = null;
+ this.manifestUrl = null;
+ this.errorMessage = null;
+ this.phase = Phase.PREPARING;
+ this.active = true;
+ publish();
+ }
+
+ /** The local cx-core opened the session: the invite is ready to render. */
+ void setReady(String sessionId, String token, String manifestUrl) {
+ this.sessionId = sessionId;
+ this.token = token;
+ this.manifestUrl = manifestUrl;
+ this.phase = Phase.READY;
+ publish();
+ }
+
+ void fail(String message) {
+ this.errorMessage = message;
+ this.phase = Phase.ERROR;
+ publish();
+ }
+
+ void clear() {
+ active = false;
+ phase = Phase.PREPARING;
+ itemCount = 0;
+ sessionId = null;
+ token = null;
+ manifestUrl = null;
+ errorMessage = null;
+ publish();
+ }
+
+ void publish() {
+ main.post(() -> {
+ if (listener != null) {
+ listener.onUpdate();
+ }
+ });
+ }
+
+ // ---- UI-side reads -------------------------------------------------------------------------
+
+ boolean hasSession() {
+ return active;
+ }
+
+ boolean isPreparing() {
+ return active && phase == Phase.PREPARING;
+ }
+
+ boolean isReady() {
+ return active && phase == Phase.READY;
+ }
+
+ boolean hasError() {
+ return active && phase == Phase.ERROR;
+ }
+
+ int itemCount() {
+ return itemCount;
+ }
+
+ String sessionId() {
+ return sessionId;
+ }
+
+ String token() {
+ return token;
+ }
+
+ String manifestUrl() {
+ return manifestUrl;
+ }
+
+ String errorMessage() {
+ return errorMessage;
+ }
+}
diff --git a/controller/app/src/main/java/org/appdevforall/k2go/notifications/NotificationIds.java b/controller/app/src/main/java/org/appdevforall/k2go/notifications/NotificationIds.java
index a2fadb7b1..0acede28f 100644
--- a/controller/app/src/main/java/org/appdevforall/k2go/notifications/NotificationIds.java
+++ b/controller/app/src/main/java/org/appdevforall/k2go/notifications/NotificationIds.java
@@ -40,6 +40,7 @@ private NotificationIds() {}
public static final int CONTENT_EXPORT = 15; // ContentExportService (K2GO-427)
public static final int CONTENT_IMPORT = 16; // ContentImportService (K2GO-427)
public static final int CONTENT_DELETE = 17; // ContentDeleteService (K2GO-427)
+ public static final int CONTENT_SHARE = 18; // ContentShareService (K2GO-376)
// High range: single-purpose notifications that re-alert in place (they replace, never stack),
// kept well away from the low range. Listed here too so the one source stays complete.
diff --git a/controller/app/src/main/res/layout/fragment_k2go_content_manager.xml b/controller/app/src/main/res/layout/fragment_k2go_content_manager.xml
index 7ee1174d8..54464ac3e 100644
--- a/controller/app/src/main/res/layout/fragment_k2go_content_manager.xml
+++ b/controller/app/src/main/res/layout/fragment_k2go_content_manager.xml
@@ -2,8 +2,8 @@
+ the same list. The bottom action bar (Export / Share / Delete) acts on the selection; the
+ buttons are enabled by the fragment. -->
-
+
-
-
diff --git a/controller/app/src/main/res/layout/fragment_k2go_content_share.xml b/controller/app/src/main/res/layout/fragment_k2go_content_share.xml
new file mode 100644
index 000000000..f6b144dd5
--- /dev/null
+++ b/controller/app/src/main/res/layout/fragment_k2go_content_share.xml
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/controller/app/src/main/res/values-ar/strings.xml b/controller/app/src/main/res/values-ar/strings.xml
index 41ed129f6..1e1396115 100644
--- a/controller/app/src/main/res/values-ar/strings.xml
+++ b/controller/app/src/main/res/values-ar/strings.xml
@@ -1500,4 +1500,24 @@
تحديد الكل في هذه المجموعةحسب الفئة
+
+ مشاركة المحتوى
+ مشاركة %1$d عنصر
+ جارٍ التحضير للمشاركة...
+ جارٍ بدء نقطة الاتصال...
+ جارٍ البحث عن عنوان نقطة الاتصال...
+ انضمام
+ استقبال
+ امسح على الهاتف الآخر للانضمام إلى نقطة الاتصال هذه.
+ ثم امسح هذا لاستقبال المحتوى.
+ لا يمكن لهذا الهاتف بدء نقطة اتصال للمشاركة.
+ تعذّر بدء نقطة الاتصال.
+ إذن نقطة الاتصال مطلوب للمشاركة.
+ تعذّر بدء المشاركة.
+ أبقِ هذه الشاشة مفتوحة حتى ينتهي الهاتف الآخر.
+ تم
+ إيقاف
+ جارٍ مشاركة المحتوى
+ تظل نقطة الاتصال والمشاركة مفتوحتين.
+ مشاركة المحتوى
diff --git a/controller/app/src/main/res/values-az/strings.xml b/controller/app/src/main/res/values-az/strings.xml
index 516a04c98..2479875ea 100644
--- a/controller/app/src/main/res/values-az/strings.xml
+++ b/controller/app/src/main/res/values-az/strings.xml
@@ -1520,4 +1520,24 @@
Bu qrupda hamısını seçKateqoriyaya görə
+
+ Məzmunu paylaş
+ %1$d element paylaşılır
+ Paylaşmağa hazırlanır...
+ Hotspot başladılır...
+ Hotspot ünvanı axtarılır...
+ Qoşul
+ Qəbul et
+ Bu hotspota qoşulmaq üçün digər telefonda skan edin.
+ Sonra məzmunu qəbul etmək üçün bunu skan edin.
+ Bu telefon paylaşmaq üçün hotspot başlada bilmir.
+ Hotspot başladıla bilmədi.
+ Paylaşmaq üçün hotspot icazəsi lazımdır.
+ Paylaşma başladıla bilmədi.
+ Digər telefon bitirənə qədər bu ekranı açıq saxlayın.
+ Hazırdır
+ Dayandır
+ Məzmun paylaşılır
+ Hotspot və paylaşma açıq qalır.
+ Məzmun paylaşımı
diff --git a/controller/app/src/main/res/values-bg/strings.xml b/controller/app/src/main/res/values-bg/strings.xml
index 43720128a..f86ad13d0 100644
--- a/controller/app/src/main/res/values-bg/strings.xml
+++ b/controller/app/src/main/res/values-bg/strings.xml
@@ -1507,4 +1507,24 @@
Избор на всички в тази групаПо категория
+
+ Споделяне на съдържание
+ Споделяне на %1$d елемента
+ Подготовка за споделяне...
+ Стартиране на точката за достъп...
+ Търсене на адреса на точката за достъп...
+ Присъединяване
+ Получаване
+ Сканирайте на другия телефон, за да се присъедините към тази точка за достъп.
+ След това сканирайте това, за да получите съдържанието.
+ Този телефон не може да стартира точка за достъп за споделяне.
+ Точката за достъп не можа да се стартира.
+ За споделяне е необходимо разрешение за точка за достъп.
+ Споделянето не можа да стартира.
+ Дръжте този екран отворен, докато другият телефон приключи.
+ Готово
+ Спиране
+ Споделяне на съдържание
+ Точката за достъп и споделянето остават отворени.
+ Споделяне на съдържание
diff --git a/controller/app/src/main/res/values-bn/strings.xml b/controller/app/src/main/res/values-bn/strings.xml
index e97bbfa3e..c17a5a079 100644
--- a/controller/app/src/main/res/values-bn/strings.xml
+++ b/controller/app/src/main/res/values-bn/strings.xml
@@ -1513,4 +1513,24 @@
এই গ্রুপের সবগুলো নির্বাচন করুনবিভাগ অনুসারে
+
+ কন্টেন্ট শেয়ার করুন
+ %1$dটি আইটেম শেয়ার করা হচ্ছে
+ শেয়ার করার প্রস্তুতি নেওয়া হচ্ছে...
+ হটস্পট চালু করা হচ্ছে...
+ হটস্পট ঠিকানা খোঁজা হচ্ছে...
+ যোগ দিন
+ গ্রহণ করুন
+ এই হটস্পটে যোগ দিতে অন্য ফোনে স্ক্যান করুন।
+ তারপর কন্টেন্ট পেতে এটি স্ক্যান করুন।
+ এই ফোনটি শেয়ার করার জন্য হটস্পট চালু করতে পারে না।
+ হটস্পট চালু করা যায়নি।
+ শেয়ার করার জন্য হটস্পট অনুমতি প্রয়োজন।
+ শেয়ার করা শুরু করা যায়নি।
+ অন্য ফোনটি শেষ না হওয়া পর্যন্ত এই স্ক্রিনটি খোলা রাখুন।
+ সম্পন্ন
+ বন্ধ করুন
+ কন্টেন্ট শেয়ার করা হচ্ছে
+ হটস্পট এবং শেয়ার খোলা থাকে।
+ কন্টেন্ট শেয়ারিং
diff --git a/controller/app/src/main/res/values-cs/strings.xml b/controller/app/src/main/res/values-cs/strings.xml
index fe34bf428..67c691755 100644
--- a/controller/app/src/main/res/values-cs/strings.xml
+++ b/controller/app/src/main/res/values-cs/strings.xml
@@ -1507,4 +1507,24 @@
Vybrat vše v této skupiněPodle kategorie
+
+ Sdílet obsah
+ Sdílení %1$d položek
+ Příprava sdílení...
+ Spouštění hotspotu...
+ Hledání adresy hotspotu...
+ Připojit se
+ Přijmout
+ Naskenujte na druhém telefonu a připojte se k tomuto hotspotu.
+ Potom naskenujte toto pro příjem obsahu.
+ Tento telefon nemůže spustit hotspot pro sdílení.
+ Hotspot se nepodařilo spustit.
+ Pro sdílení je potřeba oprávnění hotspotu.
+ Sdílení se nepodařilo spustit.
+ Nechte tuto obrazovku otevřenou, dokud druhý telefon nedokončí.
+ Hotovo
+ Zastavit
+ Sdílení obsahu
+ Hotspot a sdílení zůstávají otevřené.
+ Sdílení obsahu
diff --git a/controller/app/src/main/res/values-de/strings.xml b/controller/app/src/main/res/values-de/strings.xml
index 0133f19f2..889bef141 100644
--- a/controller/app/src/main/res/values-de/strings.xml
+++ b/controller/app/src/main/res/values-de/strings.xml
@@ -1500,4 +1500,24 @@
Alle in dieser Gruppe auswählenNach Kategorie
+
+ Inhalt teilen
+ %1$d Elemente werden geteilt
+ Teilen wird vorbereitet...
+ Hotspot wird gestartet...
+ Hotspot-Adresse wird gesucht...
+ Beitreten
+ Empfangen
+ Auf dem anderen Telefon scannen, um diesem Hotspot beizutreten.
+ Dann dies scannen, um den Inhalt zu empfangen.
+ Dieses Telefon kann keinen Hotspot zum Teilen starten.
+ Hotspot konnte nicht gestartet werden.
+ Die Hotspot-Berechtigung ist zum Teilen erforderlich.
+ Teilen konnte nicht gestartet werden.
+ Lassen Sie diesen Bildschirm geöffnet, bis das andere Telefon fertig ist.
+ Fertig
+ Stopp
+ Inhalt wird geteilt
+ Der Hotspot und die Freigabe bleiben geöffnet.
+ Inhaltsfreigabe
diff --git a/controller/app/src/main/res/values-el/strings.xml b/controller/app/src/main/res/values-el/strings.xml
index bc73a19c9..74563ead7 100644
--- a/controller/app/src/main/res/values-el/strings.xml
+++ b/controller/app/src/main/res/values-el/strings.xml
@@ -1507,4 +1507,24 @@
Επιλογή όλων σε αυτήν την ομάδαΑνά κατηγορία
+
+ Κοινή χρήση περιεχομένου
+ Κοινή χρήση %1$d στοιχείων
+ Προετοιμασία κοινής χρήσης...
+ Εκκίνηση hotspot...
+ Εύρεση διεύθυνσης hotspot...
+ Σύνδεση
+ Λήψη
+ Σαρώστε στο άλλο τηλέφωνο για να συνδεθείτε σε αυτό το hotspot.
+ Στη συνέχεια σαρώστε αυτό για να λάβετε το περιεχόμενο.
+ Αυτό το τηλέφωνο δεν μπορεί να ξεκινήσει hotspot για κοινή χρήση.
+ Δεν ήταν δυνατή η εκκίνηση του hotspot.
+ Απαιτείται η άδεια hotspot για κοινή χρήση.
+ Δεν ήταν δυνατή η έναρξη της κοινής χρήσης.
+ Κρατήστε αυτήν την οθόνη ανοιχτή μέχρι να τελειώσει το άλλο τηλέφωνο.
+ Τέλος
+ Διακοπή
+ Κοινή χρήση περιεχομένου
+ Το hotspot και η κοινή χρήση παραμένουν ανοιχτά.
+ Κοινή χρήση περιεχομένου
diff --git a/controller/app/src/main/res/values-es/strings.xml b/controller/app/src/main/res/values-es/strings.xml
index 62cac676d..74c6dfe14 100644
--- a/controller/app/src/main/res/values-es/strings.xml
+++ b/controller/app/src/main/res/values-es/strings.xml
@@ -1576,4 +1576,24 @@
Seleccionar todo en este grupoPor categoría
+
+ Compartir contenido
+ Compartiendo %1$d elementos
+ Preparando para compartir...
+ Iniciando el punto de acceso...
+ Buscando la dirección del punto de acceso...
+ Unirse
+ Recibir
+ Escanea en el otro teléfono para unirte a este punto de acceso.
+ Luego escanea esto para recibir el contenido.
+ Este teléfono no puede iniciar un punto de acceso para compartir.
+ No se pudo iniciar el punto de acceso.
+ Se necesita el permiso del punto de acceso para compartir.
+ No se pudo iniciar el uso compartido.
+ Mantén esta pantalla abierta hasta que el otro teléfono termine.
+ Listo
+ Detener
+ Compartiendo contenido
+ El punto de acceso y el uso compartido permanecen abiertos.
+ Uso compartido de contenido
diff --git a/controller/app/src/main/res/values-fa/strings.xml b/controller/app/src/main/res/values-fa/strings.xml
index f32406bf8..588acda81 100644
--- a/controller/app/src/main/res/values-fa/strings.xml
+++ b/controller/app/src/main/res/values-fa/strings.xml
@@ -1500,4 +1500,24 @@
انتخاب همه در این گروهبر اساس دسته
+
+ اشتراکگذاری محتوا
+ در حال اشتراکگذاری %1$d مورد
+ در حال آمادهسازی برای اشتراکگذاری...
+ در حال راهاندازی نقطه اتصال...
+ در حال یافتن نشانی نقطه اتصال...
+ پیوستن
+ دریافت
+ برای پیوستن به این نقطه اتصال، در تلفن دیگر اسکن کنید.
+ سپس برای دریافت محتوا این را اسکن کنید.
+ این تلفن نمیتواند برای اشتراکگذاری نقطه اتصال راهاندازی کند.
+ نقطه اتصال راهاندازی نشد.
+ برای اشتراکگذاری به اجازه نقطه اتصال نیاز است.
+ اشتراکگذاری آغاز نشد.
+ این صفحه را باز نگه دارید تا تلفن دیگر تمام شود.
+ انجام شد
+ توقف
+ در حال اشتراکگذاری محتوا
+ نقطه اتصال و اشتراکگذاری باز میمانند.
+ اشتراکگذاری محتوا
diff --git a/controller/app/src/main/res/values-fr/strings.xml b/controller/app/src/main/res/values-fr/strings.xml
index d68119f46..a9cbe52a1 100644
--- a/controller/app/src/main/res/values-fr/strings.xml
+++ b/controller/app/src/main/res/values-fr/strings.xml
@@ -1587,4 +1587,24 @@
Tout sélectionner dans ce groupePar catégorie
+
+ Partager le contenu
+ Partage de %1$d éléments
+ Préparation du partage...
+ Démarrage du point d\'accès...
+ Recherche de l\'adresse du point d\'accès...
+ Rejoindre
+ Recevoir
+ Scannez sur l\'autre téléphone pour rejoindre ce point d\'accès.
+ Ensuite, scannez ceci pour recevoir le contenu.
+ Ce téléphone ne peut pas démarrer un point d\'accès pour partager.
+ Impossible de démarrer le point d\'accès.
+ L\'autorisation du point d\'accès est nécessaire pour partager.
+ Impossible de démarrer le partage.
+ Gardez cet écran ouvert jusqu\'à ce que l\'autre téléphone ait terminé.
+ Terminé
+ Arrêter
+ Partage du contenu
+ Le point d\'accès et le partage restent ouverts.
+ Partage de contenu
diff --git a/controller/app/src/main/res/values-gu/strings.xml b/controller/app/src/main/res/values-gu/strings.xml
index f01b63b1d..e33f6798b 100644
--- a/controller/app/src/main/res/values-gu/strings.xml
+++ b/controller/app/src/main/res/values-gu/strings.xml
@@ -1513,4 +1513,24 @@
આ જૂથમાંના બધા પસંદ કરોશ્રેણી પ્રમાણે
+
+ ਸામગ્રી શેર કરો
+ %1$d આઇટમ શેર કરી રહ્યા છે
+ શેર કરવાની તૈયારી કરી રહ્યા છે...
+ હોટ્સ્પોટ શરૂ કરી રહ્યા છે...
+ હોટ્સ્પોટ સરનામું શોધી રહ્યા છે...
+ જોડાઓ
+ પ્રાપ્ત કરો
+ આ હોટ્સ્પોટમાં જોડાવા માટે બીજા ફોન પર સ્કૅન કરો.
+ પછી સામગ્રી પ્રાપ્ત કરવા માટે આ સ્કૅન કરો.
+ આ ફોન શેર કરવા માટે હોટ્સ્પોટ શરૂ કરી શકતો નથી.
+ હોટ્સ્પોટ શરૂ કરી શકાયું નથી.
+ શેર કરવા માટે હોટ્સ્પોટ પરવાની જરૂરી છે.
+ શેર કરવાનું શરૂ કરી શકાયું નથી.
+ બીજો ફોન પૂર્ણ થાય ત્યાં સુધી આ સ્ક્રીન ખુલ્લી રાખો.
+ પૂર્ણ થયું
+ બંધ કરો
+ સામગ્રી શેર કરી રહ્યા છે
+ હોટ્સ્પોટ અને શેરિંગ ખુલ્લા રહે છે.
+ સામગ્રી શેરिંગ
diff --git a/controller/app/src/main/res/values-hi/strings.xml b/controller/app/src/main/res/values-hi/strings.xml
index 99a6378bd..0f8a7ee05 100644
--- a/controller/app/src/main/res/values-hi/strings.xml
+++ b/controller/app/src/main/res/values-hi/strings.xml
@@ -1577,4 +1577,24 @@
इस समूह में सभी का चयन करेंश्रेणी के अनुसार
+
+ सामग्री साझा करें
+ %1$d आइटम साझा किए जा रहे हैं
+ साझा करने की तैयारी हो रही है...
+ हॉटस्पॉट शुरू हो रहा है...
+ हॉटस्पॉट पता खोजा जा रहा है...
+ शामिल हों
+ प्राप्त करें
+ इस हॉटस्पॉट से जुड़ने के लिए दूसरे फ़ोन पर स्कैन करें।
+ फिर सामग्री प्राप्त करने के लिए इसे स्कैन करें।
+ यह फ़ोन साझा करने के लिए हॉटस्पॉट शुरू नहीं कर सकता।
+ हॉटस्पॉट शुरू नहीं किया जा सका।
+ साझा करने के लिए हॉटस्पॉट अनुमति आवश्यक है।
+ साझा करना शुरू नहीं किया जा सका।
+ दूसरे फ़ोन के पूरा होने तक इस स्क्रीन को खुला रखें।
+ हो गया
+ रोकें
+ सामग्री साझा की जा रही है
+ हॉटस्पॉट और शेयरिंग खुले रहते हैं।
+ सामग्री शेयरिंग
diff --git a/controller/app/src/main/res/values-hu/strings.xml b/controller/app/src/main/res/values-hu/strings.xml
index 0a72052ca..ee7bd4104 100644
--- a/controller/app/src/main/res/values-hu/strings.xml
+++ b/controller/app/src/main/res/values-hu/strings.xml
@@ -1500,4 +1500,24 @@
Az összes kijelölése ebben a csoportbanKategória szerint
+
+ Tartalom megosztása
+ %1$d elem megosztása
+ Megosztás előkészítése...
+ Hotspot indítása...
+ Hotspot címének keresése...
+ Csatlakozás
+ Fogadás
+ Olvassa be a másik telefonon a hotspothoz való csatlakozáshoz.
+ Majd olvassa be ezt a tartalom fogadásához.
+ Ez a telefon nem tud hotspotot indítani a megosztáshoz.
+ A hotspot indítása nem sikerült.
+ A megosztáshoz hotspot engedély szükséges.
+ A megosztás indítása nem sikerült.
+ Tartsa nyitva ezt a képernyőt, amíg a másik telefon be nem fejezi.
+ Kész
+ Leállítás
+ Tartalom megosztása
+ A hotspot és a megosztás nyitva marad.
+ Tartalommegosztás
diff --git a/controller/app/src/main/res/values-in/strings.xml b/controller/app/src/main/res/values-in/strings.xml
index 93ac6c696..e956f33a3 100644
--- a/controller/app/src/main/res/values-in/strings.xml
+++ b/controller/app/src/main/res/values-in/strings.xml
@@ -1507,4 +1507,24 @@
Pilih semua di grup iniMenurut kategori
+
+ Bagikan konten
+ Membagikan %1$d item
+ Menyiapkan untuk membagikan...
+ Memulai hotspot...
+ Mencari alamat hotspot...
+ Gabung
+ Terima
+ Pindai di ponsel lain untuk bergabung ke hotspot ini.
+ Lalu pindai ini untuk menerima konten.
+ Ponsel ini tidak dapat memulai hotspot untuk berbagi.
+ Tidak dapat memulai hotspot.
+ Izin hotspot diperlukan untuk berbagi.
+ Tidak dapat memulai berbagi.
+ Biarkan layar ini terbuka hingga ponsel lain selesai.
+ Selesai
+ Hentikan
+ Membagikan konten
+ Hotspot dan sesi berbagi tetap terbuka.
+ Berbagi konten
diff --git a/controller/app/src/main/res/values-it/strings.xml b/controller/app/src/main/res/values-it/strings.xml
index 76db0c55b..59eb18c85 100644
--- a/controller/app/src/main/res/values-it/strings.xml
+++ b/controller/app/src/main/res/values-it/strings.xml
@@ -1500,4 +1500,24 @@
Seleziona tutto in questo gruppoPer categoria
+
+ Condividi contenuti
+ Condivisione di %1$d elementi
+ Preparazione della condivisione...
+ Avvio dell\'hotspot...
+ Ricerca dell\'indirizzo dell\'hotspot...
+ Partecipa
+ Ricevi
+ Esegui la scansione sull\'altro telefono per partecipare a questo hotspot.
+ Poi esegui la scansione di questo per ricevere i contenuti.
+ Questo telefono non può avviare un hotspot per la condivisione.
+ Impossibile avviare l\'hotspot.
+ L\'autorizzazione hotspot è necessaria per la condivisione.
+ Impossibile avviare la condivisione.
+ Tieni questa schermata aperta finché l\'altro telefono non ha terminato.
+ Fatto
+ Interrompi
+ Condivisione dei contenuti
+ L\'hotspot e la condivisione restano aperti.
+ Condivisione contenuti
diff --git a/controller/app/src/main/res/values-ja/strings.xml b/controller/app/src/main/res/values-ja/strings.xml
index a1d40957f..7579c1d30 100644
--- a/controller/app/src/main/res/values-ja/strings.xml
+++ b/controller/app/src/main/res/values-ja/strings.xml
@@ -1501,4 +1501,24 @@
このグループをすべて選択カテゴリ別
+
+ コンテンツを共有
+ %1$d 件のアイテムを共有中
+ 共有を準備しています...
+ ホットスポットを開始しています...
+ ホットスポットのアドレスを探しています...
+ 参加
+ 受信
+ このホットスポットに参加するには、もう一方のスマートフォンでスキャンしてください。
+ 次に、コンテンツを受信するにはこれをスキャンしてください。
+ このスマートフォンは共有用のホットスポットを開始できません。
+ ホットスポットを開始できませんでした。
+ 共有にはホットスポットの権限が必要です。
+ 共有を開始できませんでした。
+ もう一方のスマートフォンが完了するまで、この画面を開いたままにしてください。
+ 完了
+ 停止
+ コンテンツを共有中
+ ホットスポットと共有は開いたままになります。
+ コンテンツ共有
diff --git a/controller/app/src/main/res/values-ko/strings.xml b/controller/app/src/main/res/values-ko/strings.xml
index b947e1042..b4fff01fb 100644
--- a/controller/app/src/main/res/values-ko/strings.xml
+++ b/controller/app/src/main/res/values-ko/strings.xml
@@ -1501,4 +1501,24 @@
이 그룹에서 모두 선택카테고리별
+
+ 콘텐츠 공유
+ %1$d개 항목 공유 중
+ 공유 준비 중...
+ 핫스팅 시작 중...
+ 핫스팅 주소 찾는 중...
+ 참가
+ 받기
+ 이 핫스팅에 참가하려면 다른 휴대전화에서 스캔하세요.
+ 그런 다음 콘텐츠를 받으려면 이것을 스캔하세요.
+ 이 휴대전화는 공유용 핫스팅을 시작할 수 없습니다.
+ 핫스팅을 시작할 수 없습니다.
+ 공유하려면 핫스팅 권한이 필요합니다.
+ 공유를 시작할 수 없습니다.
+ 다른 휴대전화가 완료될 때까지 이 화면을 열어 두세요.
+ 완료
+ 중지
+ 콘텐츠 공유 중
+ 핫스팅과 공유가 열린 상태로 유지됩니다.
+ 콘텐츠 공유
diff --git a/controller/app/src/main/res/values-lt/strings.xml b/controller/app/src/main/res/values-lt/strings.xml
index d43717fc9..3ecf0b174 100644
--- a/controller/app/src/main/res/values-lt/strings.xml
+++ b/controller/app/src/main/res/values-lt/strings.xml
@@ -1517,4 +1517,24 @@
Pasirinkti visus šioje grupėjePagal kategoriją
+
+ Bendrinti turinį
+ Bendrinama %1$d elementų
+ Ruošiamasi bendrinti...
+ Paleidžiama viešoji interneto prieiga...
+ Ieškoma viešosios interneto prieigos adreso...
+ Prisijungti
+ Gauti
+ Nuskaitykite kitame telefone, kad prisijungtumėte prie šios viešosios interneto prieigos.
+ Tada nuskaitykite tai, kad gautumėte turinį.
+ Šis telefonas negali paleisti viešosios interneto prieigos bendrinimui.
+ Nepavyko paleisti viešosios interneto prieigos.
+ Bendrinti reikia viešosios interneto prieigos leidimo.
+ Nepavyko pradėti bendrinimo.
+ Laikykite šį ekraną atidarytą, kol kitas telefonas baigs.
+ Atlikta
+ Stabdyti
+ Bendrinamas turinys
+ Viešoji interneto prieiga ir bendrinimas lieka atidaryti.
+ Turinio bendrinimas
diff --git a/controller/app/src/main/res/values-nl/strings.xml b/controller/app/src/main/res/values-nl/strings.xml
index 7a65ff1e6..0424b2779 100644
--- a/controller/app/src/main/res/values-nl/strings.xml
+++ b/controller/app/src/main/res/values-nl/strings.xml
@@ -1500,4 +1500,24 @@
Alles in deze groep selecterenOp categorie
+
+ Inhoud delen
+ %1$d items delen
+ Delen voorbereiden...
+ Hotspot starten...
+ Hotspot-adres zoeken...
+ Deelnemen
+ Ontvangen
+ Scan op de andere telefoon om deel te nemen aan deze hotspot.
+ Scan dit vervolgens om de inhoud te ontvangen.
+ Deze telefoon kan geen hotspot starten om te delen.
+ Kan de hotspot niet starten.
+ De hotspot-machtiging is nodig om te delen.
+ Kan het delen niet starten.
+ Houd dit scherm open totdat de andere telefoon klaar is.
+ Klaar
+ Stoppen
+ Inhoud delen
+ De hotspot en het delen blijven open.
+ Inhoud delen
diff --git a/controller/app/src/main/res/values-no/strings.xml b/controller/app/src/main/res/values-no/strings.xml
index 7bfe6a6f2..0556896b9 100644
--- a/controller/app/src/main/res/values-no/strings.xml
+++ b/controller/app/src/main/res/values-no/strings.xml
@@ -1510,4 +1510,24 @@
Velg alle i denne gruppenEtter kategori
+
+ Del innhold
+ Deler %1$d elementer
+ Forbereder deling...
+ Starter hotspot...
+ Finner hotspot-adressen...
+ Bli med
+ Motta
+ Skann på den andre telefonen for å bli med i denne hotspoten.
+ Skann deretter dette for å motta innholdet.
+ Denne telefonen kan ikke starte en hotspot for deling.
+ Kunne ikke starte hotspoten.
+ Hotspot-tillatelsen er nødvendig for å dele.
+ Kunne ikke starte deling.
+ Hold denne skjermen åpen til den andre telefonen er ferdig.
+ Ferdig
+ Stopp
+ Deler innhold
+ Hotspoten og delingen forblir åpne.
+ Innholdsdeling
diff --git a/controller/app/src/main/res/values-pl/strings.xml b/controller/app/src/main/res/values-pl/strings.xml
index 59a4c8a5c..43057cdc8 100644
--- a/controller/app/src/main/res/values-pl/strings.xml
+++ b/controller/app/src/main/res/values-pl/strings.xml
@@ -1510,4 +1510,24 @@
Zaznacz wszystko w tej grupieWedług kategorii
+
+ Udostępnij zawartość
+ Udostępnianie %1$d elementów
+ Przygotowywanie do udostępniania...
+ Uruchamianie hotspotu...
+ Wyszukiwanie adresu hotspotu...
+ Dołącz
+ Odbierz
+ Zeskanuj na drugim telefonie, aby dołączyć do tego hotspotu.
+ Następnie zeskanuj to, aby odebrać zawartość.
+ Ten telefon nie może uruchomić hotspotu do udostępniania.
+ Nie można uruchomić hotspotu.
+ Do udostępniania wymagane jest uprawnienie hotspotu.
+ Nie można rozpocząć udostępniania.
+ Pozostaw ten ekran otwarty, aż drugi telefon zakończy.
+ Gotowe
+ Zatrzymaj
+ Udostępnianie zawartości
+ Hotspot i udostępnianie pozostają otwarte.
+ Udostępnianie zawartości
diff --git a/controller/app/src/main/res/values-pt/strings.xml b/controller/app/src/main/res/values-pt/strings.xml
index 6225d3359..f8111988e 100644
--- a/controller/app/src/main/res/values-pt/strings.xml
+++ b/controller/app/src/main/res/values-pt/strings.xml
@@ -1579,4 +1579,24 @@
Selecionar tudo neste grupoPor categoria
+
+ Partilhar conteúdo
+ A partilhar %1$d itens
+ A preparar a partilha...
+ A iniciar o hotspot...
+ A procurar o endereço do hotspot...
+ Juntar-se
+ Receber
+ Leia no outro telefone para se juntar a este hotspot.
+ Depois leia isto para receber o conteúdo.
+ Este telefone não consegue iniciar um hotspot para partilhar.
+ Não foi possível iniciar o hotspot.
+ A permissão do hotspot é necessária para partilhar.
+ Não foi possível iniciar a partilha.
+ Mantenha este ecrã aberto até o outro telefone terminar.
+ Concluído
+ Parar
+ A partilhar conteúdo
+ O hotspot e a partilha permanecem abertos.
+ Partilha de conteúdo
diff --git a/controller/app/src/main/res/values-ro/strings.xml b/controller/app/src/main/res/values-ro/strings.xml
index 220238aa7..53c7c9827 100644
--- a/controller/app/src/main/res/values-ro/strings.xml
+++ b/controller/app/src/main/res/values-ro/strings.xml
@@ -1500,4 +1500,24 @@
Selectează tot din acest grupDupă categorie
+
+ Partajează conținutul
+ Se partajează %1$d elemente
+ Se pregătește partajarea...
+ Se pornește hotspotul...
+ Se caută adresa hotspotului...
+ Alătură-te
+ Primește
+ Scanează pe celălalt telefon pentru a te alătura acestui hotspot.
+ Apoi scanează asta pentru a primi conținutul.
+ Acest telefon nu poate porni un hotspot pentru partajare.
+ Hotspotul nu a putut fi pornit.
+ Permisiunea pentru hotspot este necesară pentru partajare.
+ Partajarea nu a putut fi pornită.
+ Ține acest ecran deschis până când celălalt telefon termină.
+ Gata
+ Oprește
+ Se partajează conținutul
+ Hotspotul și partajarea rămân deschise.
+ Partajarea conținutului
diff --git a/controller/app/src/main/res/values-ru-rRU/strings.xml b/controller/app/src/main/res/values-ru-rRU/strings.xml
index a8b0d2c48..6d5baf061 100644
--- a/controller/app/src/main/res/values-ru-rRU/strings.xml
+++ b/controller/app/src/main/res/values-ru-rRU/strings.xml
@@ -1576,4 +1576,24 @@
Выбрать все в этой группеПо категории
+
+ Поделиться контентом
+ Отправка %1$d элементов
+ Подготовка к отправке...
+ Запуск точки доступа...
+ Поиск адреса точки доступа...
+ Присоединиться
+ Получить
+ Отсканируйте на другом телефоне, чтобы присоединиться к этой точке доступа.
+ Затем отсканируйте это, чтобы получить контент.
+ Этот телефон не может запустить точку доступа для обмена.
+ Не удалось запустить точку доступа.
+ Для обмена требуется разрешение на точку доступа.
+ Не удалось начать обмен.
+ Держите этот экран открытым, пока другой телефон не завершит.
+ Готово
+ Остановить
+ Отправка контента
+ Точка доступа и обмен остаются открытыми.
+ Обмен контентом
diff --git a/controller/app/src/main/res/values-sk/strings.xml b/controller/app/src/main/res/values-sk/strings.xml
index a902dd7d2..7442e6221 100644
--- a/controller/app/src/main/res/values-sk/strings.xml
+++ b/controller/app/src/main/res/values-sk/strings.xml
@@ -1507,4 +1507,24 @@
Vybrať všetko v tejto skupinePodľa kategórie
+
+ Zdieľať obsah
+ Zdieľanie %1$d položiek
+ Príprava zdieľania...
+ Spúšťanie hotspotu...
+ Hľadanie adresy hotspotu...
+ Pripojiť sa
+ Prijať
+ Naskenujte na druhom telefóne a pripojte sa k tomuto hotspotu.
+ Potom naskenujte toto na prijatie obsahu.
+ Tento telefón nemôže spustiť hotspot na zdieľanie.
+ Hotspot sa nepodarilo spustiť.
+ Na zdieľanie je potrebné povolenie hotspotu.
+ Zdieľanie sa nepodarilo spustiť.
+ Nechajte túto obrazovku otvorenú, kým druhý telefón neskončí.
+ Hotovo
+ Zastaviť
+ Zdieľanie obsahu
+ Hotspot a zdieľanie zóstajú otvorené.
+ Zdieľanie obsahu
diff --git a/controller/app/src/main/res/values-sr/strings.xml b/controller/app/src/main/res/values-sr/strings.xml
index 877f11e6a..d0a9133fe 100644
--- a/controller/app/src/main/res/values-sr/strings.xml
+++ b/controller/app/src/main/res/values-sr/strings.xml
@@ -1513,4 +1513,24 @@
Изабери све у овој групиПо категорији
+
+ Дели садржај
+ Дељење %1$d ставки
+ Припрема за дељење...
+ Покретање хотспота...
+ Тражење адресе хотспота...
+ Придружи се
+ Прими
+ Скенирај на другом телефону да се придружиш овом хотспоту.
+ Затим скенирај ово да примиш садржај.
+ Овај телефон не може да покрене хотспот за дељење.
+ Хотспот није могуће покренути.
+ За дељење је потребна дозвола за хотспот.
+ Дељење није могуће покренути.
+ Држи овај екран отворен док други телефон не заврши.
+ Готово
+ Заустави
+ Дељење садржаја
+ Хотспот и дељење остају отворени.
+ Дељење садржаја
diff --git a/controller/app/src/main/res/values-sw/strings.xml b/controller/app/src/main/res/values-sw/strings.xml
index a5670ddfc..ed6d9d570 100644
--- a/controller/app/src/main/res/values-sw/strings.xml
+++ b/controller/app/src/main/res/values-sw/strings.xml
@@ -1520,4 +1520,24 @@
Chagua zote katika kundi hiliKwa kategoria
+
+ Shiriki maudhui
+ Inashiriki vipengele %1$d
+ Inajiandaa kushiriki...
+ Inaanzisha hotspot...
+ Inatafuta anwani ya hotspot...
+ Jiunge
+ Pokea
+ Changanua kwenye simu nyingine ili kujiunga na hotspot hii.
+ Kisha changanua hii ili kupokea maudhui.
+ Simu hii haiwezi kuanzisha hotspot ya kushiriki.
+ Imeshindwa kuanzisha hotspot.
+ Ruhusa ya hotspot inahitajika ili kushiriki.
+ Imeshindwa kuanza kushiriki.
+ Weka skrini hii wazi hadi simu nyingine imalize.
+ Imekamilika
+ Simamisha
+ Inashiriki maudhui
+ Hotspot na ushirikishaji vinasalia wazi.
+ Ushirikishaji wa maudhui
diff --git a/controller/app/src/main/res/values-ta/strings.xml b/controller/app/src/main/res/values-ta/strings.xml
index 2d9e061e3..b6383935d 100644
--- a/controller/app/src/main/res/values-ta/strings.xml
+++ b/controller/app/src/main/res/values-ta/strings.xml
@@ -1520,4 +1520,24 @@
இந்தக் குழுவில் அனைத்தையும் தேர்ந்தெடுக்கவும்வகை வாரியாக
+
+ உள்ளடக்கத்தைப் பகிர்
+ %1$d உருப்படிகள் பகிரப்படுகின்றன
+ பகிர்வதற்குத் தயாராகிறது...
+ ஹாட்ஸ்பாட் தொடங்கப்படுகிறது...
+ ஹாட்ஸ்பாட் முகவரி கண்டறியப்படுகிறது...
+ இணைக
+ பெறுக
+ இந்த ஹாட்ஸ்பாட்டில் இணைய மற்றொரு ஃபோனில் ஸ்கேன் செய்யவும்.
+ பிறகு உள்ளடக்கத்தைப் பெற இதை ஸ்கேன் செய்யவும்.
+ இந்த ஃபோன் பகிர்வதற்கு ஹாட்ஸ்பாட்டைத் தொடங்க முடியாது.
+ ஹாட்ஸ்பாட்டைத் தொடங்க முடியவில்லை.
+ பகிர்வதற்கு ஹாட்ஸ்பாட் அனுமதி தேவை.
+ பகிர்வதைத் தொடங்க முடியவில்லை.
+ மற்றொரு ஃபோன் முடிக்கும் வரை இந்தத் திரையைத் திறந்தே வையுங்கள்.
+ முடிந்தது
+ நிறுத்து
+ உள்ளடக்கம் பகிரப்படுகிறது
+ ஹாட்ஸ்பாட்டும் பகிர்வும் திறந்தே இருக்கும்.
+ உள்ளடக்கப் பகிர்வு
diff --git a/controller/app/src/main/res/values-tr/strings.xml b/controller/app/src/main/res/values-tr/strings.xml
index c356e7cc1..de58c9638 100644
--- a/controller/app/src/main/res/values-tr/strings.xml
+++ b/controller/app/src/main/res/values-tr/strings.xml
@@ -1500,4 +1500,24 @@
Bu gruptaki tümünü seçKategoriye göre
+
+ İçeriği paylaş
+ %1$d öğe paylaşılıyor
+ Paylaşım hazırlanıyor...
+ Etkin nokta başlatılıyor...
+ Etkin nokta adresi bulunuyor...
+ Katıl
+ Al
+ Bu etkin noktaya katılmak için diğer telefonda tarayın.
+ Ardından içeriği almak için bunu tarayın.
+ Bu telefon paylaşım için etkin nokta başlatamıyor.
+ Etkin nokta başlatılamadı.
+ Paylaşım için etkin nokta izni gerekiyor.
+ Paylaşım başlatılamadı.
+ Diğer telefon bitene kadar bu ekranı açık tutun.
+ Bitti
+ Durdur
+ İçerik paylaşılıyor
+ Etkin nokta ve paylaşım açık kalır.
+ İçerik paylaşımı
diff --git a/controller/app/src/main/res/values-uk/strings.xml b/controller/app/src/main/res/values-uk/strings.xml
index f83ca7520..82c394293 100644
--- a/controller/app/src/main/res/values-uk/strings.xml
+++ b/controller/app/src/main/res/values-uk/strings.xml
@@ -1500,4 +1500,24 @@
Вибрати все в цій групіЗа категорією
+
+ Поділитися вмістом
+ Надсилання %1$d елементів
+ Підготовка до надсилання...
+ Запуск точки доступу...
+ Пошук адреси точки доступу...
+ Приєднатися
+ Отримати
+ Відскануйте на іншому телефоні, щоб приєднатися до цієї точки доступу.
+ Потім відскануйте це, щоб отримати вміст.
+ Цей телефон не може запустити точку доступу для надсилання.
+ Не вдалося запустити точку доступу.
+ Для надсилання потрібен дозвіл на точку доступу.
+ Не вдалося розпочати надсилання.
+ Тримайте цей екран відкритим, доки інший телефон не завершить.
+ Готово
+ Зупинити
+ Надсилання вмісту
+ Точка доступу та надсилання залишаються відкритими.
+ Обмін вмістом
diff --git a/controller/app/src/main/res/values-vi/strings.xml b/controller/app/src/main/res/values-vi/strings.xml
index 1dfac6c4d..a9c30913b 100644
--- a/controller/app/src/main/res/values-vi/strings.xml
+++ b/controller/app/src/main/res/values-vi/strings.xml
@@ -1500,4 +1500,24 @@
Chọn tất cả trong nhóm nàyTheo danh mục
+
+ Chia sẻ nội dung
+ Đang chia sẻ %1$d mục
+ Đang chuẩn bị chia sẻ...
+ Đang khởi động điểm phát sóng...
+ Đang tìm địa chỉ điểm phát sóng...
+ Tham gia
+ Nhận
+ Quét trên điện thoại kia để tham gia điểm phát sóng này.
+ Sau đó quét mã này để nhận nội dung.
+ Điện thoại này không thể khởi động điểm phát sóng để chia sẻ.
+ Không thể khởi động điểm phát sóng.
+ Cần có quyền điểm phát sóng để chia sẻ.
+ Không thể bắt đầu chia sẻ.
+ Giữ màn hình này mở cho đến khi điện thoại kia hoàn tất.
+ Xong
+ Dừng
+ Đang chia sẻ nội dung
+ Điểm phát sóng và phiên chia sẻ vẫn mở.
+ Chia sẻ nội dung
diff --git a/controller/app/src/main/res/values-yo/strings.xml b/controller/app/src/main/res/values-yo/strings.xml
index d1ad6bbaf..74547ddd9 100644
--- a/controller/app/src/main/res/values-yo/strings.xml
+++ b/controller/app/src/main/res/values-yo/strings.xml
@@ -1520,4 +1520,24 @@
Yan gbogbo rẹ̀ nínú ẹgbẹ́ yìíNípa ẹ̀ka
+
+ Pín àkóónú
+ ń pín àwọn nǹkan %1$d
+ ń múra látì pín...
+ ń bẹ̀rẹ̀ hotspot...
+ ń wá àdírẹ́sì hotspot...
+ Darapọ̀
+ Gbà
+ Ṣàyẹ̀wò lórí fóònù kejì látì darapọ̀ mọ́ hotspot yìí.
+ Lẹ́yìn náà ṣàyẹ̀wò èyí látì gba àkóónú náà.
+ Fóònù yìí kò lè bẹ̀rẹ̀ hotspot látì pín.
+ Kò lè bẹ̀rẹ̀ hotspot.
+ A nílò ìọ̀nda hotspot látì pín.
+ Kò lè bẹ̀rẹ̀ pínpín.
+ Jẹ́ kí ojú-ìwé yìí ṣí sílẹ̀ títí fóònù kejì yóò fi parí.
+ Ó parí
+ Dúró
+ ń pín àkóónú
+ Hotspot àti pínpín yóò wà ní ṣíṣí.
+ Pínpín àkóónú
diff --git a/controller/app/src/main/res/values-zh-rCN/strings.xml b/controller/app/src/main/res/values-zh-rCN/strings.xml
index 4cdec180f..84a73b505 100644
--- a/controller/app/src/main/res/values-zh-rCN/strings.xml
+++ b/controller/app/src/main/res/values-zh-rCN/strings.xml
@@ -1501,4 +1501,24 @@
选择此组中的全部按类别
+
+ 共享内容
+ 正在共享 %1$d 个项目
+ 正在准备共享...
+ 正在启动热点...
+ 正在查找热点地址...
+ 加入
+ 接收
+ 在另一部手机上扫描以加入此热点。
+ 然后扫描此码以接收内容。
+ 此手机无法启动用于共享的热点。
+ 无法启动热点。
+ 共享需要热点权限。
+ 无法开始共享。
+ 请保持此屏幕打开,直到另一部手机完成。
+ 完成
+ 停止
+ 正在共享内容
+ 热点和共享将保持打开状态。
+ 内容共享
diff --git a/controller/app/src/main/res/values/strings.xml b/controller/app/src/main/res/values/strings.xml
index bb37089af..a443e8ed3 100644
--- a/controller/app/src/main/res/values/strings.xml
+++ b/controller/app/src/main/res/values/strings.xml
@@ -1695,4 +1695,24 @@
Select all in this groupBy category
+
+ Share content
+ Sharing %1$d items
+ Preparing to share...
+ Starting the hotspot...
+ Finding the hotspot address...
+ Join
+ Receive
+ Scan on the other phone to join this hotspot.
+ Then scan this to receive the content.
+ This phone cannot start a hotspot to share.
+ Could not start the hotspot.
+ The hotspot permission is needed to share.
+ Could not start sharing.
+ Keep this screen open until the other phone finishes.
+ Done
+ Stop
+ Sharing content
+ The hotspot and the share stay open.
+ Content sharing
diff --git a/controller/app/src/test/java/org/appdevforall/k2go/contentexchange/data/ShareHandshakeTest.java b/controller/app/src/test/java/org/appdevforall/k2go/contentexchange/data/ShareHandshakeTest.java
new file mode 100644
index 000000000..6c8c33fd4
--- /dev/null
+++ b/controller/app/src/test/java/org/appdevforall/k2go/contentexchange/data/ShareHandshakeTest.java
@@ -0,0 +1,71 @@
+/*
+ * ============================================================================
+ * Name : ShareHandshakeTest.java
+ * Author : AppDevForAll
+ * Copyright : Copyright (c) 2026 AppDevForAll
+ * Description : K2GO-376. JVM unit tests for the Share QR schema (encode + parse round-trip + the
+ * fail-closed rejections). Uses the real org.json on the test classpath.
+ * ============================================================================
+ */
+package org.appdevforall.k2go.contentexchange.data;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertNotNull;
+
+import org.junit.Test;
+
+public class ShareHandshakeTest {
+
+ private static final String HOST = "192.168.49.1";
+ private static final int PORT = 8095;
+ private static final String TOKEN = "n5Xk3_Qq-7bGf2hJkLmNoPqRsTuVwXyZ0123456789AB";
+ private static final String MANIFEST = "/v1/share/abc123/manifest";
+
+ @Test
+ public void encodeThenDecodeRoundTrips() {
+ String payload = ShareHandshake.encode(HOST, PORT, TOKEN, MANIFEST);
+ assertNotNull(payload);
+ ShareHandshake.ShareInvite invite = ShareHandshake.decode(payload);
+ assertNotNull(invite);
+ assertEquals(HOST, invite.host);
+ assertEquals(PORT, invite.port);
+ assertEquals(TOKEN, invite.token);
+ assertEquals(MANIFEST, invite.manifestUrl);
+ assertEquals("http://192.168.49.1:8095", invite.baseUrl());
+ }
+
+ @Test
+ public void encodeRejectsAnInvalidInvite() {
+ assertNull(ShareHandshake.encode("10.0.0.1/evil", PORT, TOKEN, MANIFEST));
+ assertNull(ShareHandshake.encode(HOST, 0, TOKEN, MANIFEST));
+ assertNull(ShareHandshake.encode(HOST, PORT, "bad token", MANIFEST));
+ assertNull(ShareHandshake.encode(HOST, PORT, TOKEN, "no-leading-slash"));
+ }
+
+ @Test
+ public void decodeRejectsAForeignAppTag() {
+ // A sync QR, not a share invite.
+ assertNull(ShareHandshake.decode("{\"app\":\"iiab_sync\",\"ip\":\"192.168.49.1\",\"port\":8730}"));
+ }
+
+ @Test
+ public void decodeRejectsAMalformedAddress() {
+ assertNull(ShareHandshake.decode(
+ "{\"app\":\"iiab_share\",\"addr\":\"192.168.49.1\",\"token\":\"" + TOKEN
+ + "\",\"manifestUrl\":\"" + MANIFEST + "\"}")); // no :port
+ }
+
+ @Test
+ public void decodeRejectsAnInjectedToken() {
+ assertNull(ShareHandshake.decode(
+ "{\"app\":\"iiab_share\",\"addr\":\"192.168.49.1:8095\",\"token\":\"bad token\",\"manifestUrl\":\""
+ + MANIFEST + "\"}"));
+ }
+
+ @Test
+ public void decodeRejectsGarbage() {
+ assertNull(ShareHandshake.decode("not json"));
+ assertNull(ShareHandshake.decode("{}"));
+ }
+}
diff --git a/controller/app/src/test/java/org/appdevforall/k2go/contentexchange/domain/ShareInviteValidatorTest.java b/controller/app/src/test/java/org/appdevforall/k2go/contentexchange/domain/ShareInviteValidatorTest.java
new file mode 100644
index 000000000..a4292b778
--- /dev/null
+++ b/controller/app/src/test/java/org/appdevforall/k2go/contentexchange/domain/ShareInviteValidatorTest.java
@@ -0,0 +1,73 @@
+/*
+ * ============================================================================
+ * Name : ShareInviteValidatorTest.java
+ * Author : AppDevForAll
+ * Copyright : Copyright (c) 2026 AppDevForAll
+ * Description : K2GO-376. JVM unit tests for the Share invite rule (host/port/token/manifest path).
+ * ============================================================================
+ */
+package org.appdevforall.k2go.contentexchange.domain;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+public class ShareInviteValidatorTest {
+
+ private static final String HOST = "192.168.49.1";
+ private static final int PORT = 8095;
+ // url-safe base64, the shape of secrets.token_urlsafe(32).
+ private static final String TOKEN = "n5Xk3_Qq-7bGf2hJkLmNoPqRsTuVwXyZ0123456789AB";
+ private static final String MANIFEST = "/v1/share/abc123/manifest";
+
+ @Test
+ public void acceptsAWellFormedInvite() {
+ assertTrue(ShareInviteValidator.validate(HOST, PORT, TOKEN, MANIFEST).valid);
+ }
+
+ @Test
+ public void rejectsABadHost() {
+ assertFalse(ShareInviteValidator.validate("10.0.0.1/evil", PORT, TOKEN, MANIFEST).valid);
+ assertFalse(ShareInviteValidator.validate("user@host", PORT, TOKEN, MANIFEST).valid);
+ assertFalse(ShareInviteValidator.validate("", PORT, TOKEN, MANIFEST).valid);
+ }
+
+ @Test
+ public void rejectsABadPort() {
+ assertFalse(ShareInviteValidator.validate(HOST, 0, TOKEN, MANIFEST).valid);
+ assertFalse(ShareInviteValidator.validate(HOST, 70000, TOKEN, MANIFEST).valid);
+ assertFalse(ShareInviteValidator.validate(HOST, -1, TOKEN, MANIFEST).valid);
+ }
+
+ @Test
+ public void acceptsAUrlSafeToken() {
+ assertTrue(ShareInviteValidator.isValidToken("ABCdef012-_"));
+ assertTrue(ShareInviteValidator.isValidToken(TOKEN));
+ }
+
+ @Test
+ public void rejectsABadToken() {
+ assertFalse(ShareInviteValidator.isValidToken(null));
+ assertFalse(ShareInviteValidator.isValidToken(""));
+ assertFalse(ShareInviteValidator.isValidToken("has space"));
+ assertFalse(ShareInviteValidator.isValidToken("has.dot"));
+ assertFalse(ShareInviteValidator.isValidToken("new\nline"));
+ }
+
+ @Test
+ public void acceptsAServerRelativeManifestPath() {
+ assertTrue(ShareInviteValidator.isValidManifestPath("/v1/share/x/manifest"));
+ }
+
+ @Test
+ public void rejectsABadManifestPath() {
+ assertFalse(ShareInviteValidator.isValidManifestPath(null));
+ assertFalse(ShareInviteValidator.isValidManifestPath(""));
+ assertFalse(ShareInviteValidator.isValidManifestPath("v1/share/x")); // no leading slash
+ assertFalse(ShareInviteValidator.isValidManifestPath("//evil.example")); // authority
+ assertFalse(ShareInviteValidator.isValidManifestPath("/a/../../etc")); // climbs out
+ assertFalse(ShareInviteValidator.isValidManifestPath("http://evil/x")); // absolute url
+ assertFalse(ShareInviteValidator.isValidManifestPath("/has space")); // whitespace
+ }
+}