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
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:
run: |
chmod +x gradlew
./gradlew :app:testFossDebugUnitTest :app:assembleFossDebug :app:assembleFossRelease --no-daemon --stacktrace
- name: Test updater with real Linux apksigner and debug APK
- name: Test updater and fresh provisioner with real Linux apksigner and debug APK
shell: pwsh
run: |
$apk = (Resolve-Path 'app/build/outputs/apk/foss/debug/mumla-foss-debug.apk').Path
Expand All @@ -55,6 +55,7 @@ jobs:
$env:MINIMUM_TEST_EXPECTED_VERSION_NAME = $package.Groups[3].Value
./tools/verify-cellular-policy.ps1
./tests/update-minimum-device.Tests.ps1
./tests/provision-minimum-device.Tests.ps1
- name: Upload debug APK
uses: actions/upload-artifact@v4
with:
Expand Down
57 changes: 44 additions & 13 deletions .github/workflows/release-apk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,40 @@ jobs:
echo "expected_version_code must be a positive integer." >&2
exit 1
fi
- name: Checkout release tag with Humla
# workflow_dispatch always loads this workflow from the default branch. Check out that
# reviewed branch first and refuse to run repository code from a supplied tag until the
# tag has been resolved and bound to the exact current origin/main commit.
- name: Checkout reviewed main for release authorization
uses: actions/checkout@v4
with:
ref: refs/tags/${{ inputs.tag }}
submodules: recursive
ref: main
fetch-depth: 0
persist-credentials: false

- name: Bind release tag to current reviewed main
id: authorize_release
env:
RELEASE_TAG: ${{ inputs.tag }}
run: |
git fetch --force --no-tags origin \
"+refs/heads/main:refs/remotes/origin/main" \
"+refs/tags/$RELEASE_TAG:refs/tags/$RELEASE_TAG"
main_sha=$(git rev-parse --verify "refs/remotes/origin/main^{commit}")
tag_sha=$(git rev-parse --verify "refs/tags/$RELEASE_TAG^{commit}")
if [[ "$tag_sha" != "$main_sha" ]]; then
echo "Release tag must point to the exact current reviewed origin/main commit." >&2
echo "Refusing to check out or execute repository code from the supplied tag." >&2
exit 1
fi
echo "release_sha=$tag_sha" >> "$GITHUB_OUTPUT"

- name: Checkout authorized release commit with Humla
env:
RELEASE_SHA: ${{ steps.authorize_release.outputs.release_sha }}
run: |
git checkout --detach "$RELEASE_SHA"
git submodule sync --recursive
git submodule update --init --recursive
- name: Verify required signing secrets
env:
MINIMUM_RELEASE_KEYSTORE_BASE64: ${{ secrets.MINIMUM_RELEASE_KEYSTORE_BASE64 }}
Expand Down Expand Up @@ -134,25 +162,28 @@ jobs:
grep -F "versionName='$RELEASE_TAG'" "$RUNNER_TEMP/apk-badging.txt"
cp "$APK" "minimum-${RELEASE_TAG}-foss.apk"
sha256sum "minimum-${RELEASE_TAG}-foss.apk" > "minimum-${RELEASE_TAG}-foss.apk.sha256"
- name: Test Windows updater logic
- name: Test Windows updater and fresh provisioning logic
shell: pwsh
run: |
$errors = $null
$tokens = $null
[System.Management.Automation.Language.Parser]::ParseFile(
(Resolve-Path 'scripts/update-minimum-device.ps1'),
[ref]$tokens,
[ref]$errors) | Out-Null
if ($errors.Count -gt 0) {
$errors | Format-List *
throw "Updater failed PowerShell AST parsing."
foreach ($script in @('scripts/update-minimum-device.ps1', 'scripts/provision-minimum-device.ps1')) {
$errors = $null
$tokens = $null
[System.Management.Automation.Language.Parser]::ParseFile(
(Resolve-Path $script),
[ref]$tokens,
[ref]$errors) | Out-Null
if ($errors.Count -gt 0) {
$errors | Format-List *
throw "$script failed PowerShell AST parsing."
}
}
./tools/verify-cellular-policy.ps1
$env:MINIMUM_TEST_SIGNED_APK = (Resolve-Path 'app/build/outputs/apk/foss/release/mumla-foss-release.apk').Path
$env:MINIMUM_TEST_EXPECTED_APPLICATION_ID = '${{ vars.MINIMUM_RELEASE_APPLICATION_ID }}'
$env:MINIMUM_TEST_EXPECTED_VERSION_CODE = '${{ inputs.expected_version_code }}'
$env:MINIMUM_TEST_EXPECTED_VERSION_NAME = '${{ inputs.tag }}'
./tests/update-minimum-device.Tests.ps1
./tests/provision-minimum-device.Tests.ps1
- name: Build temporary Wi-Fi provisioner
run: |
./gradlew -p tools/t99-wifi-provisioner :app:assembleDebug --no-daemon --stacktrace
Expand Down
14 changes: 9 additions & 5 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:installLocation="auto">

<uses-feature
Expand All @@ -35,11 +36,9 @@
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.BROADCAST_CLOSE_SYSTEM_DIALOGS" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BROADCAST_STICKY" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
Expand Down Expand Up @@ -91,11 +90,14 @@
</intent-filter>
</activity>

<!-- Intentionally exported for controlled dedicated-device and legacy explicit-service
integrations; managed PTT/config authority remains service-owned. -->
<service
android:name=".service.MumlaService"
android:exported="true"
android:enabled="true"
android:foregroundServiceType="microphone" />
android:foregroundServiceType="microphone"
tools:ignore="ExportedService" />

<activity
android:name=".app.MumlaActivity"
Expand Down Expand Up @@ -171,11 +173,13 @@
</intent-filter>
</receiver>

<!-- PTT edges are registered by the running foreground service; identity remains static. -->
<!-- The controlled T56 firmware sends an unprotected OEM identity-key broadcast. This
profile-gated receiver opens identity UI only; PTT edges remain service-runtime-only. -->
<receiver
android:name=".radio.RadioHardwareKeyReceiver"
android:enabled="true"
android:exported="true">
android:exported="true"
tools:ignore="ExportedReceiver">
<intent-filter>
<action android:name="unipro.hotkey.p2.long" />
</intent-filter>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,19 @@ public final class RadioConnectionConfig {
private final String serviceName;
private final boolean autoConnect;
private final boolean autoReconnect;
private final int maximumTxSeconds;
private final List<Channel> channels;
private final int defaultChannelIndex;

private RadioConnectionConfig(int configVersion, String serviceName, boolean autoConnect,
boolean autoReconnect, List<Channel> channels,
boolean autoReconnect, int maximumTxSeconds,
List<Channel> channels,
int defaultChannelIndex) {
this.configVersion = configVersion;
this.serviceName = serviceName;
this.autoConnect = autoConnect;
this.autoReconnect = autoReconnect;
this.maximumTxSeconds = maximumTxSeconds;
this.channels = Collections.unmodifiableList(new ArrayList<>(channels));
this.defaultChannelIndex = defaultChannelIndex;
}
Expand All @@ -56,6 +59,8 @@ public static RadioConnectionConfig fromJson(JSONObject config) throws JSONExcep
? "Minimum"
: requireNonBlank(service.optString("name", "Minimum"), "service name");
JSONObject radio = config.getJSONObject("radio");
JSONObject ptt = config.getJSONObject("ptt");
int maximumTxSeconds = ptt.getInt("maximumTxSeconds");
String defaultChannelId = requireIdentifier(
radio.optString("defaultChannel", ""), "default channel");

Expand Down Expand Up @@ -111,6 +116,7 @@ public static RadioConnectionConfig fromJson(JSONObject config) throws JSONExcep
serviceName,
radio.optBoolean("autoConnect", false),
radio.optBoolean("autoReconnect", true),
maximumTxSeconds,
channels,
defaultChannelIndex);
}
Expand Down Expand Up @@ -312,6 +318,10 @@ public boolean isAutoReconnect() {
return autoReconnect;
}

public int getMaximumTxSeconds() {
return maximumTxSeconds;
}

public List<Channel> getChannels() {
return channels;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

package se.lublin.mumla.radio;

import android.annotation.SuppressLint;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
Expand All @@ -33,7 +34,9 @@ public final class RadioProcessWatchdog {
private RadioProcessWatchdog() {
}

@SuppressLint("MissingPermission")
public static void arm(Context context) {
// Exact alarm APIs are confined to pre-S; S+ deliberately uses the inexact API.
Context appContext = context.getApplicationContext();
AlarmManager alarmManager = (AlarmManager) appContext.getSystemService(Context.ALARM_SERVICE);
if (alarmManager == null) {
Expand Down
21 changes: 16 additions & 5 deletions app/src/main/java/se/lublin/mumla/radio/RadioShellActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import android.widget.ProgressBar;
import android.widget.TextView;

import androidx.activity.OnBackPressedCallback;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
Expand Down Expand Up @@ -328,6 +329,7 @@ public void onServiceConnected(ComponentName name, IBinder binder) {
// Service-owned room readiness survives Activity stop/start (including screen-off).
// Connection and channel observers still clear it when the actual radio state changes.
service.registerObserver(observer);
applyServicePttPolicy();
updateFromService();
maybeApplyPendingConfiguration();
maybeConnect();
Expand All @@ -348,6 +350,13 @@ protected void onCreate(Bundle savedInstanceState) {
settings = Settings.getInstance(this);
database = new MumlaSQLiteDatabase(this);
database.open();
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
// Dedicated radio Back always returns to the controlled recovery dashboard.
openRecoveryDashboard();
}
});
buildUi();
acceptIdentityToggleIntent(getIntent());
acceptPttRecoveryIntent(getIntent());
Expand Down Expand Up @@ -408,11 +417,6 @@ protected void onDestroy() {
super.onDestroy();
}

@Override
public void onBackPressed() {
openRecoveryDashboard();
}

private void openRecoveryDashboard() {
Intent recoveryDashboard = new Intent(this, MinimumHomeActivity.class)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
Expand Down Expand Up @@ -650,6 +654,7 @@ private void loadConfiguration() {

private void applyConfigurationToUi(RadioConnectionConfig loaded) {
config = loaded;
applyServicePttPolicy();
connectionRetrySuspended = false;
joinedConfiguredRoom = false;
updateServiceRoomReady(false);
Expand Down Expand Up @@ -1431,6 +1436,12 @@ private void updateServiceRoomReady(boolean ready) {
}
}

private void applyServicePttPolicy() {
if (service != null && config != null) {
service.setMaximumPttSeconds(config.getMaximumTxSeconds());
}
}

private int dp(int value) {
return (int) (value * getResources().getDisplayMetrics().density + 0.5f);
}
Expand Down
Loading
Loading