Skip to content

android: drop AppCompat and Material (#55, step B) - #58

Merged
compot-dev merged 2 commits into
masterfrom
android/drop-appcompat-material
Oct 17, 2026
Merged

compot-dev merged 2 commits into
masterfrom
android/drop-appcompat-material

Conversation

@compot-dev

Copy link
Copy Markdown
Collaborator

Step B of #55, stacked on #57 (step A). Fixes #55 together with #57.

What changes

  • MainActivity extends Activity. Theme: android:Theme.Material.Light.DarkActionBar, and android:Theme.Material in values-night, the platform equivalent of the old DayNight theme.
  • The settings bottom sheet is replaced by SettingsActivity: EditText fields with labels, a RadioGroup for UDP/TCP, a Switch for the full tunnel, and a "Show the key" check box instead of the password toggle. The fields, preference keys and validation are the same; typed values survive a rotation (the views keep their state; the saved settings are loaded only on the first start).
  • TunnelService: startForeground(id, notification, FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED) on Android 14+ (the type exists only there); before that, the two-argument form, which takes the type from the manifest. This replaces ServiceCompat.
  • The ACTION_EXITED receiver: registerReceiver(..., permission, null, RECEIVER_NOT_EXPORTED) (API 26+). Android 13+ honours the flag; on 8–12 a new signature permission org.openipc.camex.permission.INTERNAL keeps other apps out, which is what ContextCompat.registerReceiver did.
  • Dependencies: only androidx.annotation is left (@Keep on the method the native code calls).

Result (release APK built locally in the compot/android container)

v3.0.1 #57 (A) this PR
APK 2509592 1967120 513198

Almost all of what is left is the two libcamex.so (333 KB) and the launcher icons.

gradle lint reports no new errors. The one error left, ForegroundServicePermission on the <service> element, was there before.

Still to do

  • Device test (tests/android/device-test.sh) with the CI debug APK, plus a manual check that the settings screen saves and restores every field.
  • New screenshots of the settings screen for the README (docs/images/android-settings*.png).

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Android: replace AppCompat and Material with platform components

✨ Enhancement ⚙️ Configuration changes 🕐 20-40 Minutes

Grey Divider

AI Description

• Replace the settings bottom sheet with a platform settings screen while preserving saved fields
 and validation.
• Use platform themes, receiver registration, and foreground-service APIs to remove heavyweight
 AndroidX dependencies.
• Reduce the locally built release APK from 1,967,120 to 513,198 bytes.
Diagram

graph TD
  Main["Main Activity"] --> Settings["Settings Activity"] --> Prefs[("Saved Settings")] --> Tunnel["Tunnel Service"]
  Tunnel -->|"exit broadcast"| Main
  Manifest["App Manifest"] -->|"permission and registration"| Main
  Manifest -->|"service type"| Tunnel
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use a platform PreferenceFragment
  • ➕ Provides a native settings screen with built-in preference persistence.
  • ➖ Would require adapting the existing combined validation and key-entry flow.
  • ➖ Would be a larger behavioral departure from the current form.

Recommendation: Keep the platform Activity and explicit form: they remove the targeted dependencies while retaining the existing preference keys and validation. Confirm field restoration and saving on a device before merging.

Files changed (10) +311 / -71

Enhancement (6) +284 / -63
MainActivity.javaMove launcher activity to platform APIs +15/-11

Move launcher activity to platform APIs

• Extends Activity, opens SettingsActivity instead of a bottom sheet, and uses platform notification-permission and receiver APIs. The exit receiver is registered with a not-exported flag and the internal permission.

android/app/src/main/java/org/openipc/camex/MainActivity.java

SettingsActivity.javaReplace Material settings sheet with platform activity +51/-46

Replace Material settings sheet with platform activity

• Converts the former bottom-sheet settings logic to an Activity backed by platform widgets. It preserves preference keys and validation, loads saved values only on first creation, and finishes after saving.

android/app/src/main/java/org/openipc/camex/SettingsActivity.java

TunnelService.javaStart the foreground VPN service without ServiceCompat +11/-4

Start the foreground VPN service without ServiceCompat

• Uses the typed platform startForeground overload on Android 14+ and the two-argument overload below it. Adds the internal receiver-permission constant.

android/app/src/main/java/org/openipc/camex/TunnelService.java

activity_main.xmlUse the platform borderless button style +1/-1

Use the platform borderless button style

• Points the settings button’s style at the Android framework attribute instead of a library-provided theme attribute.

android/app/src/main/res/layout/activity_main.xml

activity_settings.xmlAdd a platform-widget settings layout +204/-0

Add a platform-widget settings layout

• Adds a scrollable form with labeled inputs, UDP/TCP radio buttons, a full-tunnel switch, a key-visibility checkbox, and a save button. It replaces the deleted Material bottom-sheet layout.

android/app/src/main/res/layout/activity_settings.xml

strings.xmlLabel the settings screen and key-visibility control +2/-1

Label the settings screen and key-visibility control

• Adds text for the show-key checkbox and changes the settings section comment to reflect the new screen.

android/app/src/main/res/values/strings.xml

Other (4) +27 / -8
build.gradle.ktsRemove AppCompat, Core, and Material dependencies +3/-4

Remove AppCompat, Core, and Material dependencies

• Replaces the three UI and compatibility libraries with androidx.annotation, retained for the native callback’s @Keep annotation.

android/app/build.gradle.kts

AndroidManifest.xmlDeclare settings activity and internal receiver permission +12/-0

Declare settings activity and internal receiver permission

• Registers the non-exported SettingsActivity and declares a signature-level permission used to protect the exit receiver on older Android versions.

android/app/src/main/AndroidManifest.xml

themes.xmlAdd the platform dark theme +9/-0

Add the platform dark theme

• Defines the night-mode variant of Theme.Camex using the platform Material dark theme.

android/app/src/main/res/values-night/themes.xml

themes.xmlSwitch the default theme to platform Material +3/-4

Switch the default theme to platform Material

• Replaces the Material Components DayNight parent with the platform light theme and dark action bar; the night resource supplies the dark variant.

android/app/src/main/res/values/themes.xml

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (1) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Settings crash on older Android 📎 Requirement gap ☼ Reliability
Description
MainActivity.onCreate() unconditionally calls the five-argument registerReceiver overload, which
is available only on API 33 and newer. On supported Android 8–12 devices (API 26–32), launching the
activity reaches that unavailable method before the main screen, settings, or VPN controls are
usable.
Code

android/app/src/main/java/org/openipc/camex/MainActivity.java[R60-61]

+        registerReceiver(exitReceiver, new IntentFilter(TunnelService.ACTION_EXITED),
+            TunnelService.PERMISSION_INTERNAL, null, RECEIVER_NOT_EXPORTED);
Evidence
The app declares a minimum API level of 26, and the manifest makes MainActivity the launcher. Its
onCreate() path directly invokes the flag-bearing receiver-registration overload without a version
check; TunnelService.PERMISSION_INTERNAL restricts receiver access but does not make the overload
available on API 26–32.

Replace AppCompat and Material with platform Android components
android/app/src/main/java/org/openipc/camex/MainActivity.java[60-61]
android/app/build.gradle.kts[12-14]
android/app/build.gradle.kts[11-16]
android/app/src/main/AndroidManifest.xml[28-38]
android/app/src/main/java/org/openipc/camex/MainActivity.java[39-62]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`MainActivity` uses an API-33-only receiver-registration overload during launcher activity startup, so supported Android 8–12 devices cannot open the main screen.
## Fix Focus Areas
- android/app/src/main/java/org/openipc/camex/MainActivity.java[57-61]
- android/app/build.gradle.kts[12-14]
## Recommended Fix
Guard receiver registration with an API 33 check. On API 33 and newer, use the five-argument `registerReceiver` overload with `RECEIVER_NOT_EXPORTED`; on older supported versions, use the four-argument overload with `TunnelService.PERMISSION_INTERNAL` so the receiver remains protected by the signature permission.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can see how risky a PR is at a glance with blast radius labels

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +60 to +61
registerReceiver(exitReceiver, new IntentFilter(TunnelService.ACTION_EXITED),
TunnelService.PERMISSION_INTERNAL, null, RECEIVER_NOT_EXPORTED);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Settings crash on older android 📎 Requirement gap ☼ Reliability

MainActivity.onCreate() unconditionally calls the five-argument registerReceiver overload, which
is available only on API 33 and newer. On supported Android 8–12 devices (API 26–32), launching the
activity reaches that unavailable method before the main screen, settings, or VPN controls are
usable.
Agent Prompt
## Issue description
`MainActivity` uses an API-33-only receiver-registration overload during launcher activity startup, so supported Android 8–12 devices cannot open the main screen.

## Fix Focus Areas
- android/app/src/main/java/org/openipc/camex/MainActivity.java[57-61]
- android/app/build.gradle.kts[12-14]

## Recommended Fix
Guard receiver registration with an API 33 check. On API 33 and newer, use the five-argument `registerReceiver` overload with `RECEIVER_NOT_EXPORTED`; on older supported versions, use the four-argument overload with `TunnelService.PERMISSION_INTERNAL` so the receiver remains protected by the signature permission.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@compot-dev

Copy link
Copy Markdown
Collaborator Author

Thanks, @qodo. On Settings crash on older Android: the call is

registerReceiver(exitReceiver, filter, TunnelService.PERMISSION_INTERNAL, null, RECEIVER_NOT_EXPORTED);

that is Context.registerReceiver(BroadcastReceiver, IntentFilter, String, Handler, int), which was added in API 26 together with registerReceiver(BroadcastReceiver, IntentFilter, int). minSdk is 26. API 33 added only the constant RECEIVER_NOT_EXPORTED (value 4). It is a compile-time constant, inlined into the dex; on 26–32 the flag is not checked, and the signature permission keeps other apps out. gradle lint reports no NewApi here, only the expected InlinedApi warning for the constant. ContextCompat.registerReceiver from androidx.core did the same before. No change. The device test on Android 10 (API 29) runs once this PR is retargeted to master and CI has built it.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Thanks, @qodo. On Settings crash on older Android: the call is

registerReceiver(exitReceiver, filter, TunnelService.PERMISSION_INTERNAL, null, RECEIVER_NOT_EXPORTED);

that is Context.registerReceiver(BroadcastReceiver, IntentFilter, String, Handler, int), which was added in API 26 together with registerReceiver(BroadcastReceiver, IntentFilter, int). minSdk is 26. API 33 added only the constant RECEIVER_NOT_EXPORTED (value 4). It is a compile-time constant, inlined into the dex; on 26–32 the flag is not checked, and the signature permission keeps other apps out. gradle lint reports no NewApi here, only the expected InlinedApi warning for the constant. ContextCompat.registerReceiver from androidx.core did the same before. No change. The device test on Android 10 (API 29) runs once this PR is retargeted to master and CI has built it.

Your API-level analysis is correct. Context.registerReceiver(BroadcastReceiver, IntentFilter, String, Handler, int) is available from API 26, and RECEIVER_NOT_EXPORTED is an inlined compile-time constant. Therefore, this call should not crash on API 26–32, and the signature permission provides the intended protection there. finding 1 appears to be a false positive and does not require a code change. I can’t change the finding’s status without an explicit dismissal request; no rule-management link is available for this finding.

@compot-dev
compot-dev changed the base branch from build/android-shrink-resources to master October 17, 2026 17:14
The app used AppCompat for its activity and theme and Material only
for the settings bottom sheet; together they were most of classes.dex
and resources.arsc.

- MainActivity extends Activity; the theme is the platform Material
  one, light with a dark action bar, dark in night mode (as DayNight).
- Settings are a SettingsActivity with EditText, RadioGroup, Switch and
  a "Show the key" check box instead of the bottom sheet; same fields,
  preference keys and validation. Typed values survive a rotation.
- TunnelService calls startForeground() itself: with the type on
  Android 14+, where systemExempted exists, without it before.
- The ACTION_EXITED receiver is registered with the not-exported flag
  and, for Android 8-12, a signature permission of the app, as
  ContextCompat.registerReceiver did.
- Only androidx.annotation is left (@keep for the native callback).

Release APK built locally: 1967120 -> 513198 bytes (v3.0.1: 2509592).
@compot-dev
compot-dev force-pushed the android/drop-appcompat-material branch from 797bef8 to dd9f9d2 Compare October 17, 2026 17:14
The main screen and the settings screen after dropping AppCompat and
Material; taken with the CI release APK of dd9f9d2.
@compot-dev

compot-dev commented Oct 17, 2026 •

Copy link
Copy Markdown
Collaborator Author

Retargeted to master and rebased on it (dd9f9d2, CI run 39052899407 green). Device test on the Android 10 TV box (OpenIPC lab) with the CI build:

  • tests/android/device-test.sh with camex-android-debug.apk (1315237 bytes): PASS, all checks. The server in this run used the new static camex-x86_64 from the same CI run (build: static musl release binaries for Linux, no UPX (#54) #56).
  • Release APK camex-android.apk: 517294 bytes (v3.0.1: 2509592). Installed and checked by hand through the UI:
    • every field was set to a value other than its default (host, port 5801, client name, TCP, full tunnel on, server public key, PSK, MTU 1400, keepalive 15, timeout 30) → Save → the main screen; reopening Settings shows all of them restored;
    • "Show the key" shows the PSK in clear text and hides it again;
    • MTU 100 → Save: the error "MTU must be 576–9000" is shown and the screen stays open;
    • no crash in logcat.
  • New README screenshots (main screen, settings, advanced) taken with this release APK, commit 23f2fac.

@compot-dev
compot-dev merged commit f9ff150 into master Oct 17, 2026
16 checks passed
@compot-dev
compot-dev deleted the android/drop-appcompat-material branch October 17, 2026 17:48
@compot-dev compot-dev mentioned this pull request Oct 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Android: shrink the APK from 2.5 MB — A) resource shrinking and locale filter, then B) drop AppCompat and Material

1 participant