Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions controller/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,15 @@
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<!-- ADFA-4520: LocalOnlyHotspot requires location at runtime on this targetSdk -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<!-- K2GO-438: Android 12+ lets the user grant COARSE only, so declare it with FINE (CoarseFineLocation). -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- ADFA-4520 / K2GO-439: location gated LocalOnlyHotspot only on API 32 and below; API 33+ uses
NEARBY_WIFI_DEVICES below, so scope FINE/COARSE (the hotspot is their only consumer) to API 32. -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="32" />
<!-- K2GO-438: Android 12 and below let the user grant COARSE only, so declare it with FINE (CoarseFineLocation). -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="32" />
<!-- K2GO-439: targetSdk 33+ needs NEARBY_WIFI_DEVICES for startLocalOnlyHotspot (FINE only sufficed on 32 and below).
neverForLocation: the hotspot does not derive location from Wi-Fi scans. -->
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES"
android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* ============================================================================
* Name : HotspotPermissions.java
* Author : AppDevForAll
* Copyright : Copyright (c) 2026 AppDevForAll
* Description : Single source for the runtime permission a LocalOnlyHotspot needs
* on this OS (K2GO-439). From API 33 (TIRAMISU)
* WifiManager.startLocalOnlyHotspot requires NEARBY_WIFI_DEVICES;
* API 32 and below need ACCESS_FINE_LOCATION. The Connect and Clone
* screens read this instead of each hardcoding the location gate.
* See the startLocalOnlyHotspot javadoc.
* ============================================================================
*/
package org.appdevforall.k2go.hotspot;

import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;

import androidx.core.content.ContextCompat;

/** Stateless permission rule for the LocalOnlyHotspot, shared by the Connect and Clone screens. */
public final class HotspotPermissions {

private HotspotPermissions() {}

/** The permission(s) startLocalOnlyHotspot requires on this device's API level. */
public static String[] required() {
return requiredFor(Build.VERSION.SDK_INT);
}

/**
* The rule as a pure function of the API level. Package-private for unit testing.
* Keyed off the device API: startLocalOnlyHotspot bases its requirement on the app
* targetSdk, and keying off the device API matches only because the app targets >= 33.
*/
static String[] requiredFor(int sdkInt) {
if (sdkInt >= Build.VERSION_CODES.TIRAMISU) {
return new String[]{ Manifest.permission.NEARBY_WIFI_DEVICES };
}
return new String[]{ Manifest.permission.ACCESS_FINE_LOCATION };
}

/** True when every required permission is granted. */
public static boolean granted(Context ctx) {
for (String p : required()) {
if (ContextCompat.checkSelfPermission(ctx, p) != PackageManager.PERMISSION_GRANTED) {
return false;
}
}
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
* the carrier tethering-entitlement check, so it works with no SIM and
* no data plan. The reservation is process-bound: it stays up while this
* app process is alive and is torn down on close() or process death.
* API 26+ only; callers must gate on Build.VERSION and hold
* CHANGE_WIFI_STATE + ACCESS_FINE_LOCATION (with Location services on).
* API 26+ only; callers must gate on Build.VERSION and hold CHANGE_WIFI_STATE
* plus the hotspot permission from HotspotPermissions (NEARBY_WIFI_DEVICES on
* API 33+, or ACCESS_FINE_LOCATION with Location services on 32 and below).
*
* V1 scope (ADFA-4520): manual opt-in from Advanced settings, plus a
* contextual recommendation surfaced only when BOTH conditions hold
Expand Down Expand Up @@ -110,13 +111,15 @@ public boolean isOn() {
}

/**
* Starts a LocalOnlyHotspot. Caller must have already granted CHANGE_WIFI_STATE +
* ACCESS_FINE_LOCATION and enabled Location services; otherwise onFailed fires.
* Starts a LocalOnlyHotspot. Caller must have already granted CHANGE_WIFI_STATE and the
* hotspot permission (see HotspotPermissions); on API 32 and below that also needs Location
* services on. Otherwise onFailed fires.
*/
// NewApi is suppressed because every API-26 reference below is protected by the
// explicit Build.VERSION.SDK_INT guard (lint cannot see through isSupported(), and
// @RequiresApi does not cover the anonymous callback class). MissingPermission is
// suppressed because CHANGE_WIFI_STATE + ACCESS_FINE_LOCATION are requested at runtime.
// suppressed because CHANGE_WIFI_STATE and the hotspot permission (HotspotPermissions) are
// requested at runtime.
@SuppressLint({"MissingPermission", "NewApi"})
public void start(Context context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
package org.appdevforall.k2go.redesign;

import android.Manifest;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.graphics.drawable.GradientDrawable;
import android.net.Uri;
Expand Down Expand Up @@ -42,6 +40,7 @@
import org.appdevforall.k2go.SyncHandshakeHelper;
import org.appdevforall.k2go.env.EnvironmentLock;
import org.appdevforall.k2go.sync.domain.ApkShareName;
import org.appdevforall.k2go.hotspot.HotspotPermissions;
import org.appdevforall.k2go.hotspot.LocalHotspotManager;
import org.appdevforall.k2go.sync.domain.ShareConfig;
import org.appdevforall.k2go.sync.presentation.SyncProgressRepository;
Expand Down Expand Up @@ -109,7 +108,7 @@ private enum Page { PREPARE, COPY }
private ApkServer apkServer;
private String apkFileName;

private ActivityResultLauncher<String> locationPerm;
private ActivityResultLauncher<String[]> hotspotPerm;
// ADFA-5146: request the location permission at most once per attempt. ensureHotspot() runs on every
// render, and the permission callback re-renders; on OEMs that return a denied permission result
// synchronously that becomes unbounded recursion (render -> ensureHotspot -> launch -> sync-deny ->
Expand Down Expand Up @@ -170,9 +169,14 @@ private enum RStage { JOIN, START }
@Override
public void onCreate(@Nullable Bundle s) {
super.onCreate(s);
locationPerm = registerForActivityResult(
new ActivityResultContracts.RequestPermission(),
granted -> { if (granted) hs.start(requireContext().getApplicationContext()); render(); });
hotspotPerm = registerForActivityResult(
new ActivityResultContracts.RequestMultiplePermissions(),
result -> {
if (HotspotPermissions.granted(requireContext())) {
hs.start(requireContext().getApplicationContext());
}
render();
});
barcodeLauncher = registerForActivityResult(new ScanContract(), r -> onScan(r.getContents()));
}

Expand Down Expand Up @@ -445,14 +449,13 @@ private void ensureHotspot() {
// (the "Caller already has an active LocalOnlyHotspot request" log spam).
LocalHotspotManager.State st = hs.state().getValue();
if (st != null && st.phase == LocalHotspotManager.Phase.STARTING) return;
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.ACCESS_FINE_LOCATION)
== PackageManager.PERMISSION_GRANTED) {
if (HotspotPermissions.granted(requireContext())) {
hs.start(requireContext().getApplicationContext());
} else if (!locationAsked) {
// ADFA-5146: launch the request exactly once. Do NOT re-launch on later renders (incl. the one
// the permission callback triggers) — a synchronous deny would otherwise recurse into overflow.
// the permission callback triggers): a synchronous deny would otherwise recurse into overflow.
locationAsked = true;
locationPerm.launch(Manifest.permission.ACCESS_FINE_LOCATION);
hotspotPerm.launch(HotspotPermissions.required());
}
}

Expand Down Expand Up @@ -727,7 +730,10 @@ private void renderPrepare() {
secJoin.setFallback(requireContext(), null);
secJoin.caption.setText(getString(R.string.k2go_connect_hotspot_unsupported));
secJoin.subCaption.setText(getString(R.string.k2go_connect_try_wifi));
} else if (phase == LocalHotspotManager.Phase.FAILED) {
} else if (phase == LocalHotspotManager.Phase.FAILED
|| (phase != LocalHotspotManager.Phase.ON && !HotspotPermissions.granted(requireContext()))) {
// FAILED, or the hotspot permission is not granted (denied): show the hint instead of a
// perpetual "starting" placeholder (K2GO-439).
secJoin.frame.setVisibility(View.GONE);
secJoin.setFallback(requireContext(), null);
secJoin.caption.setText(getString(R.string.k2go_connect_hotspot_failed));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package org.appdevforall.k2go.redesign;

import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.drawable.GradientDrawable;
import android.os.Bundle;
import android.view.Gravity;
Expand All @@ -19,6 +17,7 @@
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import org.appdevforall.k2go.R;
import org.appdevforall.k2go.hotspot.HotspotPermissions;
import org.appdevforall.k2go.hotspot.LocalHotspotManager;
import org.appdevforall.k2go.sync.transport.NetworkInterfaces;
import org.appdevforall.k2go.sync.transport.NetworkStateLiveData;
Expand Down Expand Up @@ -51,7 +50,7 @@ private enum Mode { HOTSPOT, WIFI }
// the QR would point at a dead port. Read on onResume (a system is not gained while this is open).

private final LocalHotspotManager hs = LocalHotspotManager.get();
private ActivityResultLauncher<String> locationPerm;
private ActivityResultLauncher<String[]> hotspotPerm;

private TextView tabHotspot, tabWifi, finish, connFooter;
// ADFA-5346: advance CTA is a MaterialButton (shape/size from the shared style); styleAdvance only
Expand All @@ -63,10 +62,12 @@ private enum Mode { HOTSPOT, WIFI }
@Override
public void onCreate(@Nullable Bundle s) {
super.onCreate(s);
locationPerm = registerForActivityResult(
new ActivityResultContracts.RequestPermission(),
granted -> {
if (granted) hs.start(requireContext().getApplicationContext());
hotspotPerm = registerForActivityResult(
new ActivityResultContracts.RequestMultiplePermissions(),
result -> {
if (HotspotPermissions.granted(requireContext())) {
hs.start(requireContext().getApplicationContext());
}
render();
});
}
Expand Down Expand Up @@ -126,11 +127,10 @@ private void setMode(Mode m) {

private void ensureHotspot() {
if (!LocalHotspotManager.isSupported() || hs.isOn()) return;
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.ACCESS_FINE_LOCATION)
== PackageManager.PERMISSION_GRANTED) {
if (HotspotPermissions.granted(requireContext())) {
hs.start(requireContext().getApplicationContext());
} else {
locationPerm.launch(Manifest.permission.ACCESS_FINE_LOCATION);
hotspotPerm.launch(HotspotPermissions.required());
}
}

Expand All @@ -141,6 +141,12 @@ public void onResume() {
super.onResume();
// ADFA-5150/5312: redraw on the way to the front — a system may have been recovered, or an
// install may have finished, while the user was away. render() re-reads the shared verdict.
// K2GO-439: if the hotspot permission was granted while away (e.g. in Settings), start it now so
// the user is not stuck on "Starting...". Only when already granted: do NOT re-launch the request
// here, which would re-prompt on every resume (ensureHotspot has no one-shot guard in Connect).
if (mode == Mode.HOTSPOT && HotspotPermissions.granted(requireContext())) {
ensureHotspot();
}
render();
}

Expand Down Expand Up @@ -216,7 +222,10 @@ private void renderHotspot() {
singleStatus(getString(R.string.k2go_connect_hotspot_unsupported), getString(R.string.k2go_connect_try_wifi));
return;
}
if (phase == LocalHotspotManager.Phase.FAILED) {
// FAILED, or the hotspot permission is not granted (denied): show the hint instead of a
// perpetual "starting" placeholder (K2GO-439).
if (phase == LocalHotspotManager.Phase.FAILED
|| (phase != LocalHotspotManager.Phase.ON && !HotspotPermissions.granted(requireContext()))) {
singleStatus(getString(R.string.k2go_connect_hotspot_failed), getString(R.string.k2go_connect_enable_location));
return;
}
Expand Down
2 changes: 1 addition & 1 deletion controller/app/src/main/res/values-ar/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -799,7 +799,7 @@
<string name="k2go_connect_starting_hotspot">جارٍ بدء نقطة الاتصال…</string>
<string name="k2go_connect_resolving_address">جارٍ العثور على عنوان نقطة الاتصال…</string>
<string name="k2go_connect_hotspot_failed">تعذّر بدء نقطة الاتصال</string>
<string name="k2go_connect_enable_location">فعّل إذن الموقع، ثم أعد المحاولة.</string>
<string name="k2go_connect_enable_location">فعّل Wi-Fi وامنح الإذن، ثم حاول مرة أخرى.</string>
<string name="k2go_connect_no_wifi">لست على شبكة Wi-Fi</string>
<string name="k2go_connect_join_wifi">انضم إلى نفس شبكة Wi-Fi أو اتصل بنقطة اتصال المكتبة.</string>
<string name="k2go_fallback_wifi">Wi-Fi: %1$s</string>
Expand Down
2 changes: 1 addition & 1 deletion controller/app/src/main/res/values-az/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -819,7 +819,7 @@
<string name="k2go_connect_starting_hotspot">Hotspot başladılır…</string>
<string name="k2go_connect_resolving_address">Hotspot ünvanı tapılır…</string>
<string name="k2go_connect_hotspot_failed">Hotspot başladıla bilmədi</string>
<string name="k2go_connect_enable_location">Məkan icazəsini aktiv edin və yenidən cəhd edin.</string>
<string name="k2go_connect_enable_location">Wi-Fi-ni yandırın və icazəni verin, sonra yenidən cəhd edin.</string>
<string name="k2go_connect_no_wifi">Wi-Fi şəbəkəsində deyil</string>
<string name="k2go_connect_join_wifi">Eyni Wi-Fi-yə və ya kitabxananın hotspotuna qoşulun.</string>
<string name="k2go_fallback_wifi">Wi-Fi: %1$s</string>
Expand Down
2 changes: 1 addition & 1 deletion controller/app/src/main/res/values-bg/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -806,7 +806,7 @@
<string name="k2go_connect_starting_hotspot">Стартиране на точката за достъп…</string>
<string name="k2go_connect_resolving_address">Търсене на адрес на точката за достъп…</string>
<string name="k2go_connect_hotspot_failed">Неуспешно стартиране на точката за достъп</string>
<string name="k2go_connect_enable_location">Включете разрешението за местоположение и опитайте отново.</string>
<string name="k2go_connect_enable_location">Включете Wi-Fi и дайте разрешението, след което опитайте отново.</string>
<string name="k2go_connect_no_wifi">Не сте в Wi-Fi мрежа</string>
<string name="k2go_connect_join_wifi">Свържете се със същата Wi-Fi или с точката за достъп на библиотеката.</string>
<string name="k2go_fallback_wifi">Wi-Fi: %1$s</string>
Expand Down
2 changes: 1 addition & 1 deletion controller/app/src/main/res/values-bn/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -812,7 +812,7 @@
<string name="k2go_connect_starting_hotspot">হটস্পট শুরু হচ্ছে…</string>
<string name="k2go_connect_resolving_address">হটস্পট ঠিকানা খোঁজা হচ্ছে…</string>
<string name="k2go_connect_hotspot_failed">হটস্পট শুরু করা যায়নি</string>
<string name="k2go_connect_enable_location">অবস্থান অনুমতি চালু করুন, তারপর আবার চেষ্টা করুন।</string>
<string name="k2go_connect_enable_location">Wi-Fi চালু করুন এবং অনুমতি দিন, তারপর আবার চেষ্টা করুন।</string>
<string name="k2go_connect_no_wifi">কোনো Wi-Fi নেটওয়ার্কে নেই</string>
<string name="k2go_connect_join_wifi">একই Wi-Fi-তে বা লাইব্রেরির হটস্পটে যুক্ত হন।</string>
<string name="k2go_fallback_wifi">Wi-Fi: %1$s</string>
Expand Down
2 changes: 1 addition & 1 deletion controller/app/src/main/res/values-cs/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -806,7 +806,7 @@
<string name="k2go_connect_starting_hotspot">Spouštění hotspotu…</string>
<string name="k2go_connect_resolving_address">Hledání adresy hotspotu…</string>
<string name="k2go_connect_hotspot_failed">Nepodařilo se spustit hotspot</string>
<string name="k2go_connect_enable_location">Povol oprávnění k poloze a zkus to znovu.</string>
<string name="k2go_connect_enable_location">Zapněte Wi-Fi a udělte oprávnění, poté to zkuste znovu.</string>
<string name="k2go_connect_no_wifi">Nepřipojeno k síti Wi-Fi</string>
<string name="k2go_connect_join_wifi">Připoj se ke stejné Wi-Fi nebo k hotspotu knihovny.</string>
<string name="k2go_fallback_wifi">Wi-Fi: %1$s</string>
Expand Down
2 changes: 1 addition & 1 deletion controller/app/src/main/res/values-de/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -799,7 +799,7 @@
<string name="k2go_connect_starting_hotspot">Hotspot wird gestartet…</string>
<string name="k2go_connect_resolving_address">Hotspot-Adresse wird gesucht…</string>
<string name="k2go_connect_hotspot_failed">Hotspot konnte nicht gestartet werden</string>
<string name="k2go_connect_enable_location">Aktiviere die Standortberechtigung und versuche es erneut.</string>
<string name="k2go_connect_enable_location">Schalten Sie WLAN ein und erteilen Sie die Berechtigung, und versuchen Sie es erneut.</string>
<string name="k2go_connect_no_wifi">Nicht in einem Wi-Fi-Netzwerk</string>
<string name="k2go_connect_join_wifi">Tritt demselben WLAN bei oder verbinde dich mit dem Hotspot der Bibliothek.</string>
<string name="k2go_fallback_wifi">Wi-Fi: %1$s</string>
Expand Down
Loading