Sunday, August 31, 2025

Celestial Illumination for Android

Back in 2020, I was contacted by a company that was sourcing Android apps for the U.S. Army. I learned about the hackathon they organized and decided to compete with a few apps (apparently, developing only one was not enough for me). During the hackathon, the SolarWinds security breach was discovered, and the whole event was canceled. We didn't manage to present the apps in all competing categories; however, this one went through (and no, it did not win).

As there is no point in keeping your work on your drive only, I decided to publish it on Google Play anyway, and you know how the story goes... I got filthy rich—again (total revenue: $4.28 USD).

Today, I returned to the app after five years of neglect and updated it to comply with the latest Google Play Store policies. Yes, the app needs a complete redesign, but I'll leave that for another time, perhaps along with open-sourcing it as well.

In any case, if you are planning some kind of special forces operation, or if you are merely a search and rescue coordinator, pilot, sailor, astronomer, photographer, farmer, outdoor adventurer, event planner, researcher or scientist, you might find my app, Celestial Illumination for Android, useful.


 

Thursday, August 21, 2025

New word puzzle game - Worderer

My new game is 'officially' released today and can be downloaded from Google Play here. Daily word puzzles will help you exercise your brain and expand your vocabulary. The top three world languages are supported: English, Spanish and Croatian. YouTube videos with game-play are here: en, es, hr.

I wrote the core of the game about a year ago, but put it aside after discovering a comparable word game that had fewer than 50 downloads in 10 months. Still, I seem to be haunted by unfinished projects, so I picked it up again and decided to release it anyway - even though I had told myself I was done with native Android development.

Give the game a try and let me know what you think!


Friday, August 15, 2025

Google Play Store app description text formatting

The StackOverflow answers here are outdated, so I have decided to take notes based on my own experiments. Here are the HTML tags that work and how they function:

  • <b>, <i> work in Google Play Store app, but not on the web.
  • <ul> doesn't work. There are no bullets inserted and opening and closing tag get interpreted as a newline character, so if you want indentation and bulleted items, use: &#8195;&#8226;&#160; (yes, there are many different space characters)


Thursday, August 14, 2025

UserVoice alternatives

Note to self: This is an export from Google Docs with the same title. It's easier to handle tables there than in Blogger. The list was collected about a year ago.

Cloud solutions

In alphabetical order …


Name

Free plan

Import from UserVoice / Comment

Aha!Ideas

No

Yes.

Canny

Yes

Free plan has 100 posts limit and admins can't comment.
They do not advertise UserVoice import functionality, but their developers can do it in the background.

Featurebase

Yes

CSV import, not all fields are supported.

Idea Vote

No


Kampsite.co

Yes

Free plan supports up to 30 suggestions.

Nolt.io

No


Prodcamp

Yes


Productboard

Yes


Savio

No


Sleekplan

Yes

CSV import, not all fields are supported.

Support Hero

No

Done via API.

Upp.vote

No


Upvoty

No


UseResponse

No


Userjot

Yes

Yes, they provide tools to help you import your existing feedback data from UserVoice. Contact their support team.

Self-hosted solutions

In alphabetical order …


Name

Status

Languages

Import from UserVoice / Comment

Astuto


TypeScript, Ruby


Feedbacker

Dead

JavaScript


Fider


Go, TypeScript

No

LogChimp

Not ready? Dead?

Vue, JavaScript


Loomio


Ruby, Vue


PHPBack

Dead

PHP


Relevant reddit thread

https://www.reddit.com/r/selfhosted/comments/1aek8yf/selfhosted_cannyio_alternative/

Sunday, August 3, 2025

Stingray attack detection on Android

Stingray snooping

A stingray (or IMSI-catcher) is a type of a surveillance device used primarily by law enforcement and intelligence agencies to intercept and track mobile phone activity. It mimics a legitimate cell phone tower, tricking nearby phones into connecting to it instead of a real tower. 

In active mode, it forces phones within a certain radius to connect to it by broadcasting a stronger signal than legitimate towers, capturing data like IMSI numbers, location, call logs, text messages, and potentially injecting malware or intercepting communications. In passive mode, it analyzes signals transmitted between phones and real towers without direct interaction. It can disrupt normal phone service, including emergency calls and collect data from all devices in range, not just targeted ones.

Stingray surveillance has been employed by government agencies since at least 1995, and it is potentially used for monitoring protests or crowds, raising privacy concerns.

For more information, here is a Wikipedia article on the Stingray phone tracker.

Stingray and Android

Recent news about upcoming Android 16 security features that can help you detect stingray attacks has prompted me to explore existing software solutions. After all, if you have a list of 'usual' cell towers around your location, you can detect a new, possibly fake tower when it appears. 

But first things first: it's worth mentioning that since Android 12, you should be able to disable 2G network connections, which are less secure and often used by stingray devices (however, new stingray devices target 4G/5G networks as well). Additionally, you should use VPNs and encrypted messaging apps (e.g., Signal) that can help secure data, although they won't prevent connection to a stingray.

Software solution

Back to how an app could detect a fake tower: it could maintain a database of known cell towers (cell IDs, MCC, MNC, LAC, signal strength, etc.) at a given location, collected over time using Android's TelephonyManager.getAllCellInfo(). There are also public databases, such as OpenCellID (Wikipedia, regarding contributing apps see my commend below) and CellMapper (app). A tower with a new or unrecognized cell ID/MCC/MNC combination not associated with known carriers in the area that wasn't previously recorded could be flagged as suspicious, especially if it has some of the following characteristics:

  • abnormally high signal strength (e.g., -50 dBm vs. -80 dBm for others) or inconsistent signal patterns;
  • protocol downgrade: stingrays often force devices to connect via less secure 2G protocols. If the app detects a sudden shift to 2G when 4G/5G was previously dominant, it’s a red flag;
  • rapid appearance/disappearance (common with mobile stingrays);
  • no neighbor consistency: legitimate towers often report neighboring towers in their signaling data. A fake tower may lack consistent neighbor data or report unusual neighbors.

Of course, legitimate towers can be added or removed by carriers (e.g., during network upgrades or temporary deployments), so a new tower isn't necessarily fake. Advanced stingrays can mimic legitimate towers closely, using valid MCC/MNC codes and spoofing known cell IDs. They may avoid 2G downgrades and operate on 4G/5G, blending in with real network behavior.

Also, Android's TelephonyManager provides limited information (e.g., cell ID, signal strength). It can't access low-level signaling data like encryption status or tower authentication. Since Android 10, privacy restrictions limit non-system apps' access to detailed cell data, making detection harder without root access.

Existing apps

Wired published an article in 2017 titled "Those Free Stingray-Detector Apps? Yeah, Spies Could Outsmart Them", which covers a study conducted by researchers from Oxford University and the Technical University of Berlin. Almost all covered detector apps (Cell Spy Catcher, AIMSICD, GSM Spy Finder and Darshak) are not maintained any more. I am not sure to what extent SnoopSnitch is maintained or whether its flaws have been addressed.

SnoopSnitch

An open-source app (GitHub link) developed by Security Research Labs (SRLabs) to detect IMSI-catchers by analyzing cell tower data and signaling behavior. It monitors for signs of fake towers, such as 2G downgrades, unencrypted connections, or silent SMS/calls (used by stingrays to ping devices). Logs tower data (cell ID, MCC, MNC, signal strength) and compares it against known patterns. Provides alerts for suspicious activity, like unexpected 2G connections or towers not matching known carrier databases. It also analyzes your phone's firmware for installed or missing Android security patches.

It requires a rooted device with a compatible Qualcomm chipset to access low-level radio data. Struggles with advanced StingRays (e.g., 4G-capable Hailstorm devices) that mimic legitimate towers closely. A 2017 study by Oxford and Berlin researchers found SnoopSnitch could be circumvented by StingRays using alternative downgrade methods or silent calls instead of SMS.

The app is available on Google Play (last updated 2023) and F-Droid (last updated 2022). There are no releases on GitHub, and the last commit was made in 2022. I was trying to access the project webpage and the list of supported devices, but the site was down at the time of writing.

AIMSICD (Android IMSI-Catcher Detector)

An open-source app (GitHub link) designed to detect IMSI-catchers by analyzing cell tower behavior and network anomalies. It detects suspicious activities like unusual tower IDs, lack of encryption, or unexpected network behavior (e.g., frequent tower switches). Maintains a local database of known towers for comparison, flagging new or unrecognized towers. Alerts users to potential Man-in-the-Middle (MITM) attacks by fake towers.

It requires root access for full functionality. It's effectiveness depends on a database of legitimate towers. It's not foolproof against advanced stingrays that spoof legitimate tower IDs or operate on 4G/5G.

Although the GitHub commits show activity, they consist solely of translations. The project has not been updated since 2017. Here is the project homepage, and here is their list of similar projects.

Darshak

Last commit about 10 years ago, here is the GitHub link. The app is not available on Google Play any more. Here is the presentation from app authors titled Darshak: How to Turn Your Phone into a Low-Cost IMSI Catcher Device.

Other (non-Android) solutions

Rayhunter

While not an Android app, the Electronic Frontier Foundation's (EFF) Rayhunter Rust tool (2025) uses an orbic mobile hotspot to detect fake towers by analyzing 4G control traffic for anomalies (e.g., suspicious IMSI requests or 2G downgrades). It's more effective than apps but requires dedicated hardware and technical setup. Project webpage, GitHub repository, news article.

Crocodile Hunter

Another open-source tool (not maintained since December 2022) from EFF that uses software-defined radio to detect stingrays by analyzing tower behavior beyond what Android's API provides. Link to GitHub repository.

Notes

OpenCellID contributing apps

The Wikipedia article on OpenCellID hasn't been updated and mentioned contributing apps are no longer active. However, there is an app named Tower Collector (available on Google Play, F-Droid and GitHub), although I haven't tried it out yet.


Tuesday, July 29, 2025

Escapa aka Pilot test

If you browsed the internet about a quarter of a century ago, you might remember this game. I have no idea who the original author is, but it is possibly one of the first JavaScript games that went viral. At that time, there were no social networks, so "going viral" actually meant being shared via email. Now it sounds so stone age, but it actually worked quite well.

Way back in 2010, I "ported" the game to Android and published it (it was my fourth Android app), and since then I have pretty much forgotten about it. It was yet another financial hit of mine - the banner ad earned me a total of $22.43 USD over those 15 years. You can't imagine how I spoiled myself with that money.

 

Recently, Google Play reminded me that I should update the game if I want to keep it in the store (why not?!), and it was also time for me to open-source it. (I have decided to clean up my old self-hosted source code versioning systems and eventually open-source everything I've written.)

So here is the game for you geezers to indulge your nostalgia. You can grab it from Google Play (the version with ads) or from GitHub (ad-free version, supporting old devices from 2012 running Android 4.0.4). 

Try to beat my score!

Sunday, July 13, 2025

Step-by-step: monitor Ko-fi traffic sources with Google Analytics

Tracking sources to your Ko-fi landing page using Google Analytics is straightforward. Simply append the appropriate utm_* parameters to your link. According to the documentation here, you should always use utm_source, utm_medium and utm_campaign. For example, the final URL might look like this:

https://ko-fi.com/<your_name>/?utm_source=<source>&utm_medium=<medium>&utm_campaign=<campaign>

Of course, replace <your_name> with your Ko-fi handle and <source>, <medium> and <campaign> with what you find suitable. That will be the link you distribute instead of the common one.

Once you have done that, head to your Google Analytics account. In the left side menu, under 'Reports', 'Generate leads', click on 'Traffic acquisition'. In the table shown, instead of 'Session primary channel group', select 'Session source/medium'.

That would be it!

Saturday, July 12, 2025

Android SDK 35 update: fixing edge-to-edge layout problems with minimal code interventions

As an Android developer with apps on the Google Play Store, you should have received an email from Google regarding the requirement to update your apps to target SDK 35 by the end of August. According to StatCounter, SDK 35 is currently present on 20% of Android devices.

In case you haven't decided to abandon your apps, there is no option but to update them. As is often the case with Android, Google tends to break things, so targeting SDK 35 also causes issues. In this post, I will focus on the enforced edge-to-edge app layout and the easiest way to opt out of it.

After updating the targetSDK and compileSDK to version 35 in your app's build.gradle file, you may have thought you were nearly finished with the necessary updates. However, upon running the app on an SDK 35 device, you might be surprised to discover that your app's layout is now rendered beneath the action bar and navigation buttons. Check out the image below for a visual reference.


Let's resolve this with minimal code changes and move on with our lives.

My demo app is quite simple, and it uses the Theme.Material3.DayNight theme. This theme is referenced directly in the AndroidManifest.xml file as follows:

<application
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.Material3.DayNight"
>
    ...

You might be referencing another theme directly, or you are defining your own theme, inheriting from an existing one. We are going exactly in that direction, as we will need to define our own theme anyway. 

First, we'll add a themes.xml file to our res/values/ directory (if it isn't there already). In that file, we'll define BaseTheme, in this example inheriting from Theme.Material3.DayNight. You will be adding general theme customizations to the BaseTheme. We will then inherit our final AppTheme from the BaseTheme (you'll see in a moment why another theme inheritance is necessary). So, themes.xml will look something like the following (you can use the styles.xml file instead, I prefer separating layout element styles from themes):

<resources>
<style name="BaseTheme" parent="Theme.Material3.DayNight">
<!-- General theme customizations go here. -->
</style>
<style name="AppTheme" parent="BaseTheme" />
</resources>

Now, we'll create the res/values-v35 directory and add another themes.xml file to it. In this file, our AppTheme will inherit from the BaseTheme as well, but it will opt out of edge-to-edge enforcement for SDK 35 devices. The file will look like the following:

<resources xmlns:tools="http://schemas.android.com/tools">
<style name="AppTheme" parent="BaseTheme">
<item
name="android:windowOptOutEdgeToEdgeEnforcement"
tools:targetApi="35">true</item>
</style>
</resources>

Finally, we need to update the AndroidManifest.xml file to reference the AppTheme:

<application
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme"
>
    ... 

And that would be it! Happy coding!

Update:

Thanks to Leon Omelan for the heads-up. The windowOptOutEdgeToEdgeEnforcement attribute, that has been added in SDK 35, has been deprecated in SDK 36, and it will not work on SDK 36 devices once you target that API level. Therefore, this solution is only temporary (just like anything else with Android).

Friday, June 20, 2025

How to reduce options menu subitem icon horizontal padding (on Android)

Your options menu might resemble something like this:

<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
>
<item
android:title="@string/language"
android:id="@+id/optionsMenuItemLanguage"
android:icon="@drawable/flag_gb_round"
>
<menu>
<item
android:title="@string/english"
android:titleCondensed="@string/english_code"
android:id="@+id/optionsSubmenuItemEnglish"
/>
<item
android:title="@string/spanish"
android:titleCondensed="@string/spanish_code"
android:id="@+id/optionsSubmenuItemSpanish"
/>
<item
android:title="@string/croatian"
android:titleCondensed="@string/croatian_code"
android:id="@+id/optionsSubmenuItemCroatian"
/>
</menu>
</item>
<item ...

Once the list of sub-menu items is opened, it will be displayed on the screen as follows:

You would probably want to enhance the usability of your options menu by adding an icon for each sub-menu item. The menu item tag supports the android:icon attribute, so the example might look like the following (for brevity, I am showing changes only for a single sub-menu item):

<item
android:title="@string/english"
android:titleCondensed="@string/english_code"
android:id="@+id/optionsSubmenuItemEnglish"
android:icon="@drawable/flag_en"
/>

Surprisingly, the menu now looks like the following:

Not sure who would be satisfied with that much padding. To keep things as simple as possible, I was looking for an XML-only solution that would work on Android devices starting from API level 21, the lowest level that I am still supporting. So, here is the solution:

For each sub-menu item icon, you will need to define a layer list. Here is an example for my case, the file is named drawables/flag_en_layer.xml:

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:left="@dimen/layer_padding"
android:right="@dimen/layer_padding"
android:drawable="@drawable/flag_en"
/>
</layer-list>

I have experimented with the padding value and found that -70dp suits my case the best. Here is the values/dimens.xml file:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="layer_padding">-70dp</dimen>
</resources>

Now, update your options menu XML file so that each item references the new layer list instead of the original drawable, as follows:

<item
android:title="@string/english"
android:titleCondensed="@string/english_code"
android:id="@+id/optionsSubmenuItemEnglish"
android:icon="@drawable/flag_en_layer"
/>

Do that for each sub-menu item, and your final menu will look like the following:

You're welcome!



Monday, May 19, 2025

Exploring the Copernicus Digital Elevation Model and alternatives to Google Elevation API

Back in March, I attended the webinar Use of Copernicus Digital Elevation Model (DEM) in Flight Procedure Design, organized by EUSPA. Unfortunately, it seems that the webinar recording is not available. Please let me know if you manage to find it somewhere.

After the webinar, I decided to experiment a bit with the Copernicus Digital Elevation Model (DEM). The API is well documented, and you can find it here:

Copernicus DEM GLO-30 Technical Characteristics, presentation slide
 

When it comes to playing with the data from Copernicus Earth Observation Programme, I usually start by using the Copernicus Browser and then switch to the Copernicus Request Builder. Once I am satisfied with the request, as the next step, I run it from the command line. Here is the request that fetches the elevation data for my hometown's peninsula (you'll need your own bearer token):

curl -X POST https://sh.dataspace.copernicus.eu/api/v1/process \
 -H 'Content-Type: application/json' \
 -H 'Authorization: Bearer <YOUR_TOKEN_HERE>' \
 -d '{
  "input": {
    "bounds": {
      "bbox": [16.381985, 43.497764, 16.441381, 43.520298],
      "crs": "http://www.opengis.net/def/crs/EPSG/0/4326"
    },
    "data": [
      {
        "type": "dem",
        "dataFilter": {
          "demInstance": "COPERNICUS_30"
        }
      }
    ]
  },
  "output": {
    "resx": 0.000277777777777778,
    "resy": 0.000277777777777778,
    "responses": [
      {
        "identifier": "default",
        "format": {
          "type": "image/tiff"
        }
      }
    ]
  },
  "evalscript": "//VERSION=3\n\nfunction setup() {\n  return {\n    input: [\"DEM\"],\n    output: {\n      id: \"default\",\n      bands: 1,\n      sampleType: \"FLOAT32\"\n    }\n  }\n}\n\nfunction evaluatePixel(sample) {\n  return [sample.DEM]\n}"
}'

You can execute the request by running:
sh request.sh > output.tif

The result is GeoTIFF file with elevation data, which in my case looks like as the following image:

Resulting tif image

To extract elevation points with Python, you will need the rasterio and numpy libraries. Here is the complete code of a script that converts pixels to (longitude, latitude, elevation) points, prints them out, along with the maximum elevation found:

import rasterio
import numpy as np

try:
with rasterio.open("output.tif") as src:

elev_data = src.read(1)
transform = src.transform
width, height = src.width, src.height
min_lon, min_lat = 16.381985, 43.497764
max_lon, max_lat = 16.441381, 43.520298

lon_step = (max_lon - min_lon) / width
lat_step = (max_lat - min_lat) / height
points = []
step = 1

for row in range(0, height, step):
for col in range(0, width, step):
lon = min_lon + col * lon_step
lat = max_lat - row * lat_step
elevation = elev_data[row, col]
if elevation != -9999:
points.append((lon, lat, elevation))

print("Elevation points (lon, lat, elevation):")
for point in points:
if point[2] != 0:
print(point)

max_elevation = max(point[2] for point in points)
print("\nMax elevation:", max_elevation)

except rasterio.errors.RasterioIOError:
print("Error: Could not open output.tif")

As I progressed, I thought, why not create an elevation API service to provide an alternative to the Google Elevation API? Fortunately, I decided to conduct some market research first. In addition to Google, I discovered around 10 other alternatives. Here’s a list of elevation API providers, presented in no particular order:

Google maps elevation API

JawgMaps

At the moment, the only elevation API provider listed is under European alternatives.

DemAPI

This one covers Netherlands only. 

Open-Elevation

It offers both an API and a self-hosted solution. I believe it has a 250m resolution, and it mentions "higher resolution" for the professional tier. It appears that the project is abandoned, but there is a maintained fork. While checking the issue tracker, I discovered a source of LiDAR elevation data for Europe.

Open-meteo elevation API

Another API plus self-hosted solution. It has 90m resolution as it uses GLO-90 datasets.

Open topo data 

API and self-hosted solution, this time with 30m resolution.

GPXZ Elevation API

Commercial sister of Open Topo Data. Parts of Europe and the US are covered with 1m LiDAR resolution, while GLO-30 is used for the rest of the globe. It can be hosted on EU-only servers if needed.

DEM.Net Elevation API

30m resolution, check it out for interesting 3D visualizations.

TessaDEM Elevation API

30m resolution

Maptoolkit Elevation API

Germany and Austria based, servers are in EU. They are listed on RapidAPI marketplace, that was recently acquired by Nokia. 

Bing Maps Elevations API

Apparently, it was not a commercial success for Microsoft, as they are retiring it. The free plan will shut down on June 30, 2025, and the enterprise plan one year after that. However, they have included a link to the step-by-step article on how to Create elevation data & services on Azure Cloud (30m resolution).

 

I'll conclude this post with a few more things:

The domain elevation.eu is taken and is being sold for €2,000 + VAT.
The contacts for the EGNOS service adoption team are:
egnos-adoption@essp-sas.eu / +34 91 627 88 63 / +34 91 627 88 59.