An Android app running on Android and on Codename One side by side

This post is mostly about phones that don’t have Google at all: GrapheneOS, LineageOS, CalyxOS, /e/OS and other Android builds. Codename One now supports them officially. After that, a shorter update on the Android compatibility layer, which runs existing Android apps on iPhone, desktop and the web.

GrapheneOS, LineageOS, CalyxOS and /e/OS

I was a member of Team OS/2. I know what it’s like to use an operating system the big vendors forgot, and to find that half the apps you need just don’t start. People choose GrapheneOS, LineageOS and other AOSP builds for good reasons, usually privacy, and they deserve apps that work. There’s a business case too: when you support a niche operating system its users have learned to live without, you get the most loyal advocates your product will ever have. They notice, they remember, and they tell each other.

Codename One apps mostly worked on these phones before. But mostly isn’t support: call the wrong API and you could hit a failure, or behavior nobody could predict. Now it’s official, with a clear boundary. One APK serves both kinds of phone: at run time it checks whether Google Play Services is there. No build hint, no flavor, no second build. The Android port never calls Play services, Play Core or Firebase without checking first, and CI enforces that, so a future change can’t quietly break it.

FeatureWith Google PlayWithout Google Play
UI, storage, networking, SQLite, mediaWorksWorks
On device ML Kit modelsWorksWorks
Location and geofencesFused locationFramework LocationManager
MapsGoogle MapsVector MapView
Google sign inNativeBrowser OAuth flow
PushFirebaseUnifiedPush (ntfy, NextPush, Sunup)
In-app purchasePlay BillingNot available
Integrity attestation, in-app reviewWorksNot available

The location fallback fixed something on Google phones too: builds without Play location now get proximity alerts and background location through the framework API.

Push, with one line

Push needs one line of configuration, because the phone has no Firebase to deliver it. UnifiedPush lets the user pick the app that holds the push connection, called a distributor. A UnifiedPush endpoint is a standard Web Push subscription, so the Codename One push service delivers to it with the Web Push credential your app already has. You copy its public key into a build hint:

codename1.arg.android.unifiedPush.vapidKey=<your 87 character VAPID public key>

Your app code doesn’t change, and neither does your server. The registration order is automatic:

flowchart TD S[PushClient registers] --> G{Google Play Services?} G -->|yes| F[Firebase Cloud Messaging] G -->|no| H{Huawei HMS?} H -->|yes| K[Huawei Push Kit] H -->|no| U{UnifiedPush distributor installed?} U -->|yes| P[UnifiedPush, delivered as Web Push] U -->|no| E[pushRegistrationError: a push service is needed]

The one real gap is in-app purchase. Play Billing needs the Play Store. If selling on de-Googled phones matters to you, tell us; it’s something we can add.

To test, use the Android emulator’s AOSP system images, the default target, which have no Google apps. The Android without Google Play chapter has the details.

Also new: classic Android apps, on iPhone

Three weeks ago we showed that an Android Studio module’s src/main can build unmodified as a Codename One app. There’s no Android runtime inside it. Views are Codename One components, layouts follow Android’s measure and layout rules, and resources compile to a binary table at build time. Nothing parses XML or uses reflection on the device. You import a project with one command:

mvn cn1:import-android-project -Dcn1.android.import=/path/to/MyAndroidApp
mvn cn1:build -Dcodename1.platform=ios
A Material 3 screen on Android and on Codename One
Material 3.
A RecyclerView on Android and on Codename One
RecyclerView.
A Kotlin activity on Android and on Codename One
Kotlin activities.

Each pair shows Android on the left and Codename One on the right, from the same source.

What’s new: the parts real apps lean on

Toy apps are mostly UI. Real apps store data, run work in the background and talk to the hardware. The layer now covers those:

  • Room. The build generates the _Impl classes, the way Android’s annotation processor does. Features we don’t support yet (@Relation, suspend DAOs, Flow, Rx, Paging, @AutoMigration and FTS) fail the build and name the member.
  • WorkManager. Chains, unique work, retries and LiveData status, persisted across app restarts.
  • Location and sensors. LocationManager and SensorManager.
  • Services. Manifest services, including startForeground() mapped to Codename One’s ForegroundService. Notifications from NotificationManager and NotificationCompat show as local notifications.
  • Keystore. AES keys from AndroidKeyStore with GCM and CBC.
  • Document pickers. ACTION_OPEN_DOCUMENT, ACTION_CREATE_DOCUMENT and ACTION_GET_CONTENT, plus the ActivityResult contracts, open the platform’s native open and save dialogs.

So this is ordinary Android code, and it now runs on an iPhone:

@Dao
public interface NoteDao {
    @Query("SELECT * FROM Note ORDER BY created DESC")
    List<Note> all();

    @Insert
    long insert(Note note);
}

Intent save = new Intent(Intent.ACTION_CREATE_DOCUMENT);
save.addCategory(Intent.CATEGORY_OPENABLE);
save.setType("text/plain");
save.putExtra(Intent.EXTRA_TITLE, "notes.txt");
startActivityForResult(save, SAVE_NOTES);

Where iOS says no

Some Android behavior has no iOS equivalent, and the Android chapter has a table for it. Location in a foreground service keeps running in the background when the manifest asks for background location. Other foreground service work gets only the short window iOS allows. Constrained WorkManager jobs go to iOS’s background task scheduler, and iOS decides when they run. On iOS, ACTION_CREATE_DOCUMENT returns RESULT_OK right away and shows the save dialog when your app closes the output stream, so a cancel isn’t reported back.

Other limits: Keystore keys aren’t hardware backed there, Parcel is an in process container, SQLite uses a single connection, there are no system content providers or broadcasts, and reflection based libraries won’t run on iOS.

Tomorrow, the last post of the week: Caches, Certificates, Streaming Uploads and a Simulator That Types.