Monday, November 3, 2025

Open-sourcing Image Converter for Android

Last update: 2025.11.14

Original post 2025.11.03

I built Image Converter during a hackathon in a short amount of time and published it in November 2020. Since it did not generate any revenue (no subscriptions sold, totaling $22.50 from ads over five years), I largely forgot about it. However, Google reminded me that I should update it to target the latest SDK, or else face consequences. Additionally, I decided to move it as the final project from self-hosted SVN to GitHub, five years after its initial launch. As a result, I have decided to open-source it as well, marking the ninth Android app that I have open-sourced!

AdMob earnings

Please note that the published version still targets Android SDK 30 and will not work on newer Android devices, as it uses direct storage access, which was once allowed. I haven't updated it yet, as it is not a priority for me. Meanwhile, users with slightly older Android devices can still enjoy the free, open-source, and lightweight image converter. Just follow the link below:

Image Converter on GitHub

Update 2025.11.14

Sooner than originally planned, I have updated Image Converter to target Android SDK 35. To make my life easier, I have decided to drop support for older devices, so the latest release requires a minimum Android SDK of 29. The version for older devices will remain downloadable here: Image Converter release 1.0.6.

The app has also been visually updated to support Material Design 3 with Dynamic colors.

Old version screenshots
New version screenshots

Another personal milestone has been reached: the first app that has been built on Linux instead of on a Windows system.

 


Sunday, October 26, 2025

Summer of 2025 swimming

No Indian summer this year — (Alt+0151) at least, that's my subjective impression. My swimming season started on June 1 and finished on September 22. I used to stretch it until November, but in the past few years, we haven't had a proper fall and have gone straight from summer to winter.

To sum it up:
18 × 0.5 km
51 × 2.5 km

... makes 136.5 km. That's 833.5 km since 2021. Looks like Greece next year.


Thursday, October 23, 2025

Using FFmpeg to add audio to your video

I have decided to add some background music to my smartphone gameplay casts, and it turns out that it's easiest to do this with ffmpeg. It's just a two-step process:

  1. Find out the video length.

    ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 input.mp4

  2. Add background music with a fade-out effect (starting two seconds before the video ends, D-2 should be the video length returned by the previous command, minus two seconds).

    ffmpeg -i input.mp4 -i input.mp3 -map 0:v -map 1:a -c:v copy -c:a aac -af "afade=t=out:st=D-2:d=2" -shortest output.mp4

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

Last updated: 2025.11.01

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!

2025.09.20 update

One month since launch!
Downloads so far: 10
Daily Active Users: 0

My current marketing approach has been posting in puzzle and game-related subreddits, but unfortunately most of those posts end up getting auto-filtered 😅. Getting visibility is harder than coding the game itself.

2025.11.01 update

Following advice from social media, I decided to promote Worderer by posting daily gameplay clips — not the latest daily challenges (as that wouldn’t make much sense), but the easiest ones from random previous days. I’ve been doing this for the past month.

Two months after launch, here are the results of my marketing efforts (31 videos posted):

  • Downloads: <50 (still)
  • Installed audience: 9
  • Monthly active users: 4

Here is how it went on each social network:

Bluesky — I couldn’t open an account without getting blocked.

Facebook (Worderer Page) — No interaction with my short videos, post insights showed only a handful of views at best.

Instagram — Zero followers, two likes across 31 posts. Shorts from the past week had 0 views. The most-viewed video (340 views) was posted on the second day.

Mastodon — No interaction at all.

TikTok — The most-liked video (2 likes) had just 15 views and was posted on the final day. The video with the most views (308) came the day before. Views improved slightly when I added background music — I found ragtime to be a good fit, since it’s light, cheerful, and often in the public domain (though one video still got muted on both TikTok and YouTube due to a false copyright claim). Only recently has TikTok allowed me to like other videos, and I still can’t follow anyone. Currently: 3 followers, 7 likes total.

X-Twitter — No followers, no likes. Statistics show between 0–8 views per post.

YouTube — Two longer videos (not shorts) got 0 and 2 views. Shorts ranged from 0–100 views, with only the first one receiving a like.

Since the number of downloads and active users didn’t increase, it’s clear that one month of daily posting hasn’t had much effect.

One general takeaway: it’s better to post screencasts in dark mode, since some platforms display video descriptions in white text.

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. For example:

/?utm_source=imageconverter&utm_medium=fdroid&utm_campaign=regular

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!