diff --git a/.github/workflows/drankcounter-release.yml b/.github/workflows/drankcounter-release.yml
new file mode 100644
index 00000000..69744682
--- /dev/null
+++ b/.github/workflows/drankcounter-release.yml
@@ -0,0 +1,40 @@
+name: drankcounter-release
+
+on:
+ workflow_dispatch:
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: write
+
+jobs:
+ drankcounter-release:
+ runs-on: ubuntu-24.04
+ steps:
+ - uses: actions/checkout@v4
+ - name: Install bob
+ run: cargo install --git https://github.com/bplaat/crates.git bob
+ - name: Setup Android SDK
+ uses: android-actions/setup-android@v3
+ with:
+ cmdline-tools-version: 14742923
+ - name: Build release
+ working-directory: bin/drankcounter
+ run: |
+ export JAVA_HOME=$JAVA_HOME_21_X64
+ export PATH=$JAVA_HOME/bin:$PATH
+ echo "${{ secrets.DRANKCOUNTER_KEYSTORE }}" | base64 --decode > keystore.jks
+ bob build --release
+ - name: Create Git tag and GitHub Release
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ working-directory: bin/drankcounter
+ run: |
+ TAG_VERSION=$(sed -nE 's/^[[:space:]]*version[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' bob.toml)
+ gh release create "drankcounter/v${TAG_VERSION}" \
+ --title "DrankCounter v${TAG_VERSION}" \
+ --notes "Download the \`.apk\` file and open it to install the application" \
+ target/release/drankcounter-${TAG_VERSION}.apk
diff --git a/LICENSE b/LICENSE
index 908dd7f2..6f129937 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2018-2025 Bastiaan van der Plaat
+Copyright (c) 2018-2026 Bastiaan van der Plaat
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 84a1eff1..e34d0743 100644
--- a/README.md
+++ b/README.md
@@ -31,13 +31,19 @@ A collection of various Android apps that I created for myself and others
+
+ 
+ DrankCounter
+
+ |
+
+
+

HackerNews
|
-
-

@@ -45,12 +51,6 @@ A collection of various Android apps that I created for myself and others
|
-
- 
- ReactTest
-
- |
-

RedSquare
@@ -75,6 +75,7 @@ A collection of various Android apps that I created for myself and others
- [BassieTest](bin/bassietest/) A example test app for the bob build tool and sandbox for some ideas
- [Bible](bin/bible/) An offline Android Bible app containing multiple bible translations
- [CoinList](bin/coinlist/) A cryptocurrency information app similar to the [coinlist](https://github.com/bplaat/coinlist) website
+- [DrankCounter](bin/drankcounter/) A basic alcohol tracker app with home screen widget
- [HackerNews](bin/hackernews/) A simple [HackerNews](https://news.ycombinator.com/) webview app because installed PWA's suck sadly
- [NOS](bin/nos/) A simple sync-once [NOS](https://nos.nl/) feed reader app
- [ReactTest](bin/reacttest/) A test app for the [ReactDroid](lib/reactdroid/) library
@@ -102,6 +103,6 @@ A collection of various Android apps that I created for myself and others
## License
-Copyright © 2018-2025 [Bastiaan van der Plaat](https://github.com/bplaat)
+Copyright © 2018-2026 [Bastiaan van der Plaat](https://github.com/bplaat)
Licensed under the [MIT](LICENSE) license.
diff --git a/bin/drankcounter/AndroidManifest.xml b/bin/drankcounter/AndroidManifest.xml
new file mode 100644
index 00000000..38c1b99b
--- /dev/null
+++ b/bin/drankcounter/AndroidManifest.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bin/drankcounter/README.md b/bin/drankcounter/README.md
new file mode 100644
index 00000000..5a557a72
--- /dev/null
+++ b/bin/drankcounter/README.md
@@ -0,0 +1,3 @@
+# DrankCounter Android App
+
+A basic alcohol tracker app with home screen widget
diff --git a/bin/drankcounter/bob.toml b/bin/drankcounter/bob.toml
new file mode 100644
index 00000000..d44f25c9
--- /dev/null
+++ b/bin/drankcounter/bob.toml
@@ -0,0 +1,12 @@
+[package]
+name = "drankcounter"
+id = "nl.plaatsoft.drankcounter"
+version = "1.0.0"
+
+[dependencies]
+alerts = { path = "../../lib/alerts" }
+compat = { path = "../../lib/compat" }
+jspecify = { maven = "org.jspecify:jspecify:1.0.0" }
+
+[package.metadata.android]
+main_activity = ".activities.MainActivity"
diff --git a/bin/drankcounter/docs/images/icon-maskable.svg b/bin/drankcounter/docs/images/icon-maskable.svg
new file mode 100644
index 00000000..fd7ef404
--- /dev/null
+++ b/bin/drankcounter/docs/images/icon-maskable.svg
@@ -0,0 +1,7 @@
+
+
diff --git a/bin/drankcounter/docs/images/icon.svg b/bin/drankcounter/docs/images/icon.svg
new file mode 100644
index 00000000..78d09ba4
--- /dev/null
+++ b/bin/drankcounter/docs/images/icon.svg
@@ -0,0 +1,13 @@
+
+
diff --git a/bin/drankcounter/res/drawable-anydpi/ic_account_supervisor.xml b/bin/drankcounter/res/drawable-anydpi/ic_account_supervisor.xml
new file mode 100644
index 00000000..a24a59c8
--- /dev/null
+++ b/bin/drankcounter/res/drawable-anydpi/ic_account_supervisor.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/bin/drankcounter/res/drawable-anydpi/ic_arrow_left.xml b/bin/drankcounter/res/drawable-anydpi/ic_arrow_left.xml
new file mode 100644
index 00000000..c0b69291
--- /dev/null
+++ b/bin/drankcounter/res/drawable-anydpi/ic_arrow_left.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/bin/drankcounter/res/drawable-anydpi/ic_cup.xml b/bin/drankcounter/res/drawable-anydpi/ic_cup.xml
new file mode 100644
index 00000000..1440e211
--- /dev/null
+++ b/bin/drankcounter/res/drawable-anydpi/ic_cup.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/bin/drankcounter/res/drawable-anydpi/ic_cup_white.xml b/bin/drankcounter/res/drawable-anydpi/ic_cup_white.xml
new file mode 100644
index 00000000..b55bb219
--- /dev/null
+++ b/bin/drankcounter/res/drawable-anydpi/ic_cup_white.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/bin/drankcounter/res/drawable-anydpi/ic_dots_vertical.xml b/bin/drankcounter/res/drawable-anydpi/ic_dots_vertical.xml
new file mode 100644
index 00000000..2ff93fd6
--- /dev/null
+++ b/bin/drankcounter/res/drawable-anydpi/ic_dots_vertical.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/bin/drankcounter/res/drawable-anydpi/ic_glass_mug.xml b/bin/drankcounter/res/drawable-anydpi/ic_glass_mug.xml
new file mode 100644
index 00000000..6c5c9a39
--- /dev/null
+++ b/bin/drankcounter/res/drawable-anydpi/ic_glass_mug.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/bin/drankcounter/res/drawable-anydpi/ic_glass_mug_white.xml b/bin/drankcounter/res/drawable-anydpi/ic_glass_mug_white.xml
new file mode 100644
index 00000000..fda4777b
--- /dev/null
+++ b/bin/drankcounter/res/drawable-anydpi/ic_glass_mug_white.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/bin/drankcounter/res/drawable-anydpi/ic_glass_wine.xml b/bin/drankcounter/res/drawable-anydpi/ic_glass_wine.xml
new file mode 100644
index 00000000..69185592
--- /dev/null
+++ b/bin/drankcounter/res/drawable-anydpi/ic_glass_wine.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/bin/drankcounter/res/drawable-anydpi/ic_glass_wine_white.xml b/bin/drankcounter/res/drawable-anydpi/ic_glass_wine_white.xml
new file mode 100644
index 00000000..c09a7d7d
--- /dev/null
+++ b/bin/drankcounter/res/drawable-anydpi/ic_glass_wine_white.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/bin/drankcounter/res/drawable-anydpi/ic_information.xml b/bin/drankcounter/res/drawable-anydpi/ic_information.xml
new file mode 100644
index 00000000..bc7e9810
--- /dev/null
+++ b/bin/drankcounter/res/drawable-anydpi/ic_information.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/bin/drankcounter/res/drawable-anydpi/ic_invert_colors.xml b/bin/drankcounter/res/drawable-anydpi/ic_invert_colors.xml
new file mode 100644
index 00000000..052dc564
--- /dev/null
+++ b/bin/drankcounter/res/drawable-anydpi/ic_invert_colors.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/bin/drankcounter/res/drawable-anydpi/ic_launcher_foreground.xml b/bin/drankcounter/res/drawable-anydpi/ic_launcher_foreground.xml
new file mode 100644
index 00000000..31ea9d88
--- /dev/null
+++ b/bin/drankcounter/res/drawable-anydpi/ic_launcher_foreground.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/bin/drankcounter/res/drawable-anydpi/ic_plus.xml b/bin/drankcounter/res/drawable-anydpi/ic_plus.xml
new file mode 100644
index 00000000..beb8df90
--- /dev/null
+++ b/bin/drankcounter/res/drawable-anydpi/ic_plus.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/bin/drankcounter/res/drawable-anydpi/ic_share_variant.xml b/bin/drankcounter/res/drawable-anydpi/ic_share_variant.xml
new file mode 100644
index 00000000..355c9f93
--- /dev/null
+++ b/bin/drankcounter/res/drawable-anydpi/ic_share_variant.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/bin/drankcounter/res/drawable-anydpi/ic_star.xml b/bin/drankcounter/res/drawable-anydpi/ic_star.xml
new file mode 100644
index 00000000..3422aa05
--- /dev/null
+++ b/bin/drankcounter/res/drawable-anydpi/ic_star.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/bin/drankcounter/res/drawable-anydpi/ic_web.xml b/bin/drankcounter/res/drawable-anydpi/ic_web.xml
new file mode 100644
index 00000000..39b40751
--- /dev/null
+++ b/bin/drankcounter/res/drawable-anydpi/ic_web.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/bin/drankcounter/res/drawable/app_bar_icon_button_ripple.xml b/bin/drankcounter/res/drawable/app_bar_icon_button_ripple.xml
new file mode 100644
index 00000000..81e0df40
--- /dev/null
+++ b/bin/drankcounter/res/drawable/app_bar_icon_button_ripple.xml
@@ -0,0 +1,4 @@
+
+
diff --git a/bin/drankcounter/res/drawable/widget_background.xml b/bin/drankcounter/res/drawable/widget_background.xml
new file mode 100644
index 00000000..6faf0f02
--- /dev/null
+++ b/bin/drankcounter/res/drawable/widget_background.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
diff --git a/bin/drankcounter/res/layout-night/widget_drankcounter.xml b/bin/drankcounter/res/layout-night/widget_drankcounter.xml
new file mode 100644
index 00000000..719a9d88
--- /dev/null
+++ b/bin/drankcounter/res/layout-night/widget_drankcounter.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bin/drankcounter/res/layout/activity_main.xml b/bin/drankcounter/res/layout/activity_main.xml
new file mode 100644
index 00000000..9c579329
--- /dev/null
+++ b/bin/drankcounter/res/layout/activity_main.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bin/drankcounter/res/layout/activity_settings.xml b/bin/drankcounter/res/layout/activity_settings.xml
new file mode 100644
index 00000000..400d2adb
--- /dev/null
+++ b/bin/drankcounter/res/layout/activity_settings.xml
@@ -0,0 +1,109 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bin/drankcounter/res/layout/item_drink.xml b/bin/drankcounter/res/layout/item_drink.xml
new file mode 100644
index 00000000..ba787a03
--- /dev/null
+++ b/bin/drankcounter/res/layout/item_drink.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bin/drankcounter/res/layout/item_drink_section_header.xml b/bin/drankcounter/res/layout/item_drink_section_header.xml
new file mode 100644
index 00000000..7e690036
--- /dev/null
+++ b/bin/drankcounter/res/layout/item_drink_section_header.xml
@@ -0,0 +1,3 @@
+
+
diff --git a/bin/drankcounter/res/layout/main_drink_header.xml b/bin/drankcounter/res/layout/main_drink_header.xml
new file mode 100644
index 00000000..406a2e27
--- /dev/null
+++ b/bin/drankcounter/res/layout/main_drink_header.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bin/drankcounter/res/layout/widget_drankcounter.xml b/bin/drankcounter/res/layout/widget_drankcounter.xml
new file mode 100644
index 00000000..719a9d88
--- /dev/null
+++ b/bin/drankcounter/res/layout/widget_drankcounter.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bin/drankcounter/res/menu/item_drink_options.xml b/bin/drankcounter/res/menu/item_drink_options.xml
new file mode 100644
index 00000000..7bbfac16
--- /dev/null
+++ b/bin/drankcounter/res/menu/item_drink_options.xml
@@ -0,0 +1,5 @@
+
+
diff --git a/bin/drankcounter/res/menu/options.xml b/bin/drankcounter/res/menu/options.xml
new file mode 100644
index 00000000..59d23d08
--- /dev/null
+++ b/bin/drankcounter/res/menu/options.xml
@@ -0,0 +1,5 @@
+
+
diff --git a/bin/drankcounter/res/mipmap-anydpi-v26/ic_launcher.xml b/bin/drankcounter/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 00000000..35c2a772
--- /dev/null
+++ b/bin/drankcounter/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/bin/drankcounter/res/mipmap-hdpi/ic_launcher.png b/bin/drankcounter/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 00000000..6426a355
Binary files /dev/null and b/bin/drankcounter/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/bin/drankcounter/res/mipmap-mdpi/ic_launcher.png b/bin/drankcounter/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 00000000..19366f0c
Binary files /dev/null and b/bin/drankcounter/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/bin/drankcounter/res/mipmap-xhdpi/ic_launcher.png b/bin/drankcounter/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 00000000..da8c3369
Binary files /dev/null and b/bin/drankcounter/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/bin/drankcounter/res/mipmap-xxhdpi/ic_launcher.png b/bin/drankcounter/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000..3c65f87d
Binary files /dev/null and b/bin/drankcounter/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/bin/drankcounter/res/mipmap-xxxhdpi/ic_launcher.png b/bin/drankcounter/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 00000000..732d945a
Binary files /dev/null and b/bin/drankcounter/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/bin/drankcounter/res/values-night/colors.xml b/bin/drankcounter/res/values-night/colors.xml
new file mode 100644
index 00000000..7f2b6a1b
--- /dev/null
+++ b/bin/drankcounter/res/values-night/colors.xml
@@ -0,0 +1,11 @@
+
+
+ #222
+ #111
+ #fff
+ #888
+ #fff
+ #333
+ #1E1E1E
+ #2B2B2B
+
diff --git a/bin/drankcounter/res/values-night/themes.xml b/bin/drankcounter/res/values-night/themes.xml
new file mode 100644
index 00000000..21bf2f02
--- /dev/null
+++ b/bin/drankcounter/res/values-night/themes.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/bin/drankcounter/res/values-nl/strings.xml b/bin/drankcounter/res/values-nl/strings.xml
new file mode 100644
index 00000000..d78ce97c
--- /dev/null
+++ b/bin/drankcounter/res/values-nl/strings.xml
@@ -0,0 +1,54 @@
+
+
+
+ Instellingen
+
+
+ Bier
+ Wijn
+ Sterke drank
+ Nog geen drankjes geregistreerd
+
+
+ Vandaag
+ Gisteren
+
+
+ Bier
+ Wijn
+ Sterke drank
+
+
+ Verwijderen
+
+
+ Instellingen
+
+ Weergave
+
+ Taal
+ Gebruik systeemstandaard
+
+ Thema
+ Licht
+ Donker
+ Ingesteld door batterijbesparing
+ Gebruik systeemstandaard
+
+ Informatie
+ App versie
+ Who, jij bent een geweldige hacker!
+ Beoordeel deze app
+ Deel deze app
+ Bekijk deze coole app: %s
+ Over deze app
+ Deze applicatie is gemaakt door Bastiaan van der Plaat
+
+ Over deze app
+ Deze applicatie is gemaakt door Bastiaan van der Plaat
+ Website
+ OK
+
+
+ Registreer je dagelijkse drankjes
+
diff --git a/bin/drankcounter/res/values/colors.xml b/bin/drankcounter/res/values/colors.xml
new file mode 100644
index 00000000..4df0b0b8
--- /dev/null
+++ b/bin/drankcounter/res/values/colors.xml
@@ -0,0 +1,13 @@
+
+
+ #D32F2F
+ #fff
+ #D32F2F
+ #fff
+ #111
+ #777
+ #333
+ #ddd
+ #F0F0F0
+ #FFFFFF
+
diff --git a/bin/drankcounter/res/values/strings.xml b/bin/drankcounter/res/values/strings.xml
new file mode 100644
index 00000000..cdf30d21
--- /dev/null
+++ b/bin/drankcounter/res/values/strings.xml
@@ -0,0 +1,58 @@
+
+
+ DrankCounter
+
+
+ Settings
+
+
+ Beer
+ Wine
+ Liqueur
+ No drinks recorded yet
+
+
+ Today
+ Yesterday
+
+
+ Beer
+ Wine
+ Liqueur
+
+
+ Delete
+
+
+ Settings
+
+ Display
+
+ Language
+ English
+ Nederlands
+ Use system default
+
+ Theme
+ Light
+ Dark
+ Set by Battery Saver
+ Use system default
+
+ Information
+ App version
+ Who, you are an amazing hacker!
+ Rate this app
+ Share this app
+ Check out this cool app: %s
+ About this app
+ This application is made by Bastiaan van der Plaat
+
+ About this app
+ This application is made by Bastiaan van der Plaat
+ Website
+ OK
+
+
+ Track your daily drinks
+
diff --git a/bin/drankcounter/res/values/styles.xml b/bin/drankcounter/res/values/styles.xml
new file mode 100644
index 00000000..108f4e68
--- /dev/null
+++ b/bin/drankcounter/res/values/styles.xml
@@ -0,0 +1,237 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bin/drankcounter/res/values/themes.xml b/bin/drankcounter/res/values/themes.xml
new file mode 100644
index 00000000..e15f65a8
--- /dev/null
+++ b/bin/drankcounter/res/values/themes.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
diff --git a/bin/drankcounter/res/xml/widget_drankcounter.xml b/bin/drankcounter/res/xml/widget_drankcounter.xml
new file mode 100644
index 00000000..4d509294
--- /dev/null
+++ b/bin/drankcounter/res/xml/widget_drankcounter.xml
@@ -0,0 +1,11 @@
+
+
diff --git a/bin/drankcounter/src/nl/plaatsoft/drankcounter/DrinkDatabase.java b/bin/drankcounter/src/nl/plaatsoft/drankcounter/DrinkDatabase.java
new file mode 100644
index 00000000..7db88be9
--- /dev/null
+++ b/bin/drankcounter/src/nl/plaatsoft/drankcounter/DrinkDatabase.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2026 Bastiaan van der Plaat
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+package nl.plaatsoft.drankcounter;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+public class DrinkDatabase extends SQLiteOpenHelper {
+ private static final String DB_NAME = "drankcounter.db";
+ private static final int DB_VERSION = 1;
+
+ public static final String TABLE_DRINKS = "drinks";
+ public static final String COLUMN_ID = "id";
+ public static final String COLUMN_TYPE = "type";
+ public static final String COLUMN_CREATED_AT = "created_at";
+
+ public DrinkDatabase(Context context) {
+ super(context, DB_NAME, null, DB_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ var createTableSQL = "CREATE TABLE " + TABLE_DRINKS + " (" + COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ + COLUMN_TYPE + " INTEGER NOT NULL, " + COLUMN_CREATED_AT + " INTEGER NOT NULL)";
+ db.execSQL(createTableSQL);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ // No upgrades for now
+ }
+}
diff --git a/bin/drankcounter/src/nl/plaatsoft/drankcounter/DrinkDatabaseHelper.java b/bin/drankcounter/src/nl/plaatsoft/drankcounter/DrinkDatabaseHelper.java
new file mode 100644
index 00000000..8fd5b35e
--- /dev/null
+++ b/bin/drankcounter/src/nl/plaatsoft/drankcounter/DrinkDatabaseHelper.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (c) 2026 Bastiaan van der Plaat
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+package nl.plaatsoft.drankcounter;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.List;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+
+import nl.plaatsoft.drankcounter.models.Drink;
+
+public class DrinkDatabaseHelper {
+ private final DrinkDatabase dbHelper;
+
+ public DrinkDatabaseHelper(Context context) {
+ this.dbHelper = new DrinkDatabase(context);
+ }
+
+ public long insertDrink(int type, long createdAt) {
+ var db = dbHelper.getWritableDatabase();
+ var values = new ContentValues();
+ values.put(DrinkDatabase.COLUMN_TYPE, type);
+ values.put(DrinkDatabase.COLUMN_CREATED_AT, createdAt);
+ return db.insert(DrinkDatabase.TABLE_DRINKS, null, values);
+ }
+
+ public List getAllDrinks() {
+ var db = dbHelper.getReadableDatabase();
+ var drinks = new ArrayList();
+ var cursor = db.query(
+ DrinkDatabase.TABLE_DRINKS, null, null, null, null, null, DrinkDatabase.COLUMN_CREATED_AT + " DESC");
+ if (cursor.moveToFirst()) {
+ do {
+ var id = cursor.getLong(cursor.getColumnIndexOrThrow(DrinkDatabase.COLUMN_ID));
+ var type = cursor.getInt(cursor.getColumnIndexOrThrow(DrinkDatabase.COLUMN_TYPE));
+ var createdAt = cursor.getLong(cursor.getColumnIndexOrThrow(DrinkDatabase.COLUMN_CREATED_AT));
+ drinks.add(new Drink(id, type, createdAt));
+ } while (cursor.moveToNext());
+ }
+ cursor.close();
+ return drinks;
+ }
+
+ public List getTodaysDrinks() {
+ var cal = Calendar.getInstance();
+ cal.set(Calendar.HOUR_OF_DAY, 0);
+ cal.set(Calendar.MINUTE, 0);
+ cal.set(Calendar.SECOND, 0);
+ cal.set(Calendar.MILLISECOND, 0);
+ var startOfDay = cal.getTimeInMillis() / 1000;
+ var db = dbHelper.getReadableDatabase();
+ var drinks = new ArrayList();
+ var cursor = db.query(DrinkDatabase.TABLE_DRINKS, null, DrinkDatabase.COLUMN_CREATED_AT + " >= ?",
+ new String[] {String.valueOf(startOfDay)}, null, null, DrinkDatabase.COLUMN_CREATED_AT + " DESC");
+ if (cursor.moveToFirst()) {
+ do {
+ var id = cursor.getLong(cursor.getColumnIndexOrThrow(DrinkDatabase.COLUMN_ID));
+ var type = cursor.getInt(cursor.getColumnIndexOrThrow(DrinkDatabase.COLUMN_TYPE));
+ var createdAt = cursor.getLong(cursor.getColumnIndexOrThrow(DrinkDatabase.COLUMN_CREATED_AT));
+ drinks.add(new Drink(id, type, createdAt));
+ } while (cursor.moveToNext());
+ }
+ cursor.close();
+ return drinks;
+ }
+
+ public static int countByType(List drinks, int type) {
+ var count = 0;
+ for (var drink : drinks) {
+ if (drink.type() == type)
+ count++;
+ }
+ return count;
+ }
+
+ public void deleteDrink(long id) {
+ var db = dbHelper.getWritableDatabase();
+ db.delete(DrinkDatabase.TABLE_DRINKS, DrinkDatabase.COLUMN_ID + " = ?", new String[] {String.valueOf(id)});
+ }
+
+ public void close() {
+ dbHelper.close();
+ }
+}
diff --git a/bin/drankcounter/src/nl/plaatsoft/drankcounter/Settings.java b/bin/drankcounter/src/nl/plaatsoft/drankcounter/Settings.java
new file mode 100644
index 00000000..8aa6ffdb
--- /dev/null
+++ b/bin/drankcounter/src/nl/plaatsoft/drankcounter/Settings.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2026 Bastiaan van der Plaat
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+package nl.plaatsoft.drankcounter;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+public class Settings {
+ private final SharedPreferences prefs;
+
+ public Settings(Context context) {
+ prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE);
+ }
+
+ // Language
+ public static final int LANGUAGE_ENGLISH = 0;
+ public static final int LANGUAGE_DUTCH = 1;
+ public static final int LANGUAGE_SYSTEM = 2;
+
+ public int getLanguage() {
+ return prefs.getInt("language", LANGUAGE_SYSTEM);
+ }
+
+ public void setLanguage(int language) {
+ prefs.edit().putInt("language", language).apply();
+ }
+
+ // Theme
+ public static final int THEME_LIGHT = 0;
+ public static final int THEME_DARK = 1;
+ public static final int THEME_SYSTEM = 2;
+
+ public int getTheme() {
+ return prefs.getInt("theme", THEME_SYSTEM);
+ }
+
+ public void setTheme(int theme) {
+ prefs.edit().putInt("theme", theme).apply();
+ }
+}
diff --git a/bin/drankcounter/src/nl/plaatsoft/drankcounter/WidgetProvider.java b/bin/drankcounter/src/nl/plaatsoft/drankcounter/WidgetProvider.java
new file mode 100644
index 00000000..17ee4241
--- /dev/null
+++ b/bin/drankcounter/src/nl/plaatsoft/drankcounter/WidgetProvider.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) 2026 Bastiaan van der Plaat
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+package nl.plaatsoft.drankcounter;
+
+import android.app.PendingIntent;
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProvider;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.widget.RemoteViews;
+
+import nl.plaatsoft.drankcounter.models.Drink;
+
+public class WidgetProvider extends AppWidgetProvider {
+ private static final String ACTION_ADD_DRINK = "nl.plaatsoft.drankcounter.ACTION_ADD_DRINK";
+ private static final String EXTRA_DRINK_TYPE = "drink_type";
+
+ @Override
+ public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
+ for (var id : appWidgetIds) {
+ updateWidget(context, appWidgetManager, id);
+ }
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ super.onReceive(context, intent);
+ if (ACTION_ADD_DRINK.equals(intent.getAction())) {
+ var type = intent.getIntExtra(EXTRA_DRINK_TYPE, Drink.TYPE_BEER);
+ var dbHelper = new DrinkDatabaseHelper(context);
+ dbHelper.insertDrink(type, System.currentTimeMillis() / 1000);
+ dbHelper.close();
+ updateAllWidgets(context);
+ }
+ }
+
+ public static void updateAllWidgets(Context context) {
+ var appWidgetManager = AppWidgetManager.getInstance(context);
+ var ids = appWidgetManager.getAppWidgetIds(new ComponentName(context, WidgetProvider.class));
+ for (var id : ids) {
+ updateWidget(context, appWidgetManager, id);
+ }
+ }
+
+ private static void updateWidget(Context context, AppWidgetManager appWidgetManager, int widgetId) {
+ var dbHelper = new DrinkDatabaseHelper(context);
+ var todayDrinks = dbHelper.getTodaysDrinks();
+ dbHelper.close();
+
+ var beerCount = DrinkDatabaseHelper.countByType(todayDrinks, Drink.TYPE_BEER);
+ var wineCount = DrinkDatabaseHelper.countByType(todayDrinks, Drink.TYPE_WINE);
+ var liqueurCount = DrinkDatabaseHelper.countByType(todayDrinks, Drink.TYPE_LIQUEUR);
+
+ var lightViews = new RemoteViews(context.getPackageName(), R.layout.widget_drankcounter);
+ applyCountsAndClicks(context, lightViews, beerCount, wineCount, liqueurCount);
+
+ RemoteViews views;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ var darkViews = new RemoteViews(context.getPackageName(), R.layout.widget_drankcounter);
+ applyCountsAndClicks(context, darkViews, beerCount, wineCount, liqueurCount);
+ views = new RemoteViews(lightViews, darkViews);
+ } else {
+ views = lightViews;
+ }
+
+ appWidgetManager.updateAppWidget(widgetId, views);
+ }
+
+ private static void applyCountsAndClicks(
+ Context context, RemoteViews views, int beerCount, int wineCount, int liqueurCount) {
+ views.setTextViewText(R.id.widget_beer_count, String.valueOf(beerCount));
+ views.setTextViewText(R.id.widget_wine_count, String.valueOf(wineCount));
+ views.setTextViewText(R.id.widget_liqueur_count, String.valueOf(liqueurCount));
+
+ views.setOnClickPendingIntent(R.id.widget_beer_button, buildAddDrinkIntent(context, Drink.TYPE_BEER));
+ views.setOnClickPendingIntent(R.id.widget_wine_button, buildAddDrinkIntent(context, Drink.TYPE_WINE));
+ views.setOnClickPendingIntent(R.id.widget_liqueur_button, buildAddDrinkIntent(context, Drink.TYPE_LIQUEUR));
+ }
+
+ private static PendingIntent buildAddDrinkIntent(Context context, int drinkType) {
+ var intent = new Intent(context, WidgetProvider.class);
+ intent.setAction(ACTION_ADD_DRINK);
+ intent.putExtra(EXTRA_DRINK_TYPE, drinkType);
+ var flags = PendingIntent.FLAG_UPDATE_CURRENT;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ flags |= PendingIntent.FLAG_IMMUTABLE;
+ }
+ return PendingIntent.getBroadcast(context, drinkType, intent, flags);
+ }
+}
diff --git a/bin/drankcounter/src/nl/plaatsoft/drankcounter/activities/BaseActivity.java b/bin/drankcounter/src/nl/plaatsoft/drankcounter/activities/BaseActivity.java
new file mode 100644
index 00000000..13cfcf40
--- /dev/null
+++ b/bin/drankcounter/src/nl/plaatsoft/drankcounter/activities/BaseActivity.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2026 Bastiaan van der Plaat
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+package nl.plaatsoft.drankcounter.activities;
+
+import java.util.Locale;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.os.Build;
+import android.os.PowerManager;
+import android.view.ViewGroup;
+import android.window.OnBackInvokedCallback;
+import android.window.OnBackInvokedDispatcher;
+
+import nl.plaatsoft.android.compat.CompatActivity;
+import nl.plaatsoft.android.compat.WindowInsetsCompat;
+import nl.plaatsoft.drankcounter.Settings;
+
+import org.jspecify.annotations.Nullable;
+
+public abstract class BaseActivity extends CompatActivity {
+ protected @SuppressWarnings("null") Settings settings;
+
+ @Override
+ public void attachBaseContext(@SuppressWarnings("null") Context context) {
+ settings = new Settings(context);
+ var language = settings.getLanguage();
+ var theme = settings.getTheme();
+
+ // Update configuration when different from system defaults
+ if (language != Settings.LANGUAGE_SYSTEM || theme != Settings.THEME_SYSTEM
+ || (theme == Settings.THEME_SYSTEM && Build.VERSION.SDK_INT < Build.VERSION_CODES.Q)) {
+ var configuration = new Configuration(context.getResources().getConfiguration());
+
+ if (language == Settings.LANGUAGE_ENGLISH)
+ configuration.setLocale(Locale.forLanguageTag("en"));
+ if (language == Settings.LANGUAGE_DUTCH)
+ configuration.setLocale(Locale.forLanguageTag("nl"));
+
+ if (theme == Settings.THEME_LIGHT) {
+ configuration.uiMode |= Configuration.UI_MODE_NIGHT_NO;
+ configuration.uiMode &= ~Configuration.UI_MODE_NIGHT_YES;
+ }
+ if (theme == Settings.THEME_DARK) {
+ configuration.uiMode |= Configuration.UI_MODE_NIGHT_YES;
+ configuration.uiMode &= ~Configuration.UI_MODE_NIGHT_NO;
+ }
+ // Set dark mode on when in battery saver mode
+ if (theme == Settings.THEME_SYSTEM && Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
+ if (((PowerManager)context.getSystemService(Context.POWER_SERVICE)).isPowerSaveMode()) {
+ configuration.uiMode |= Configuration.UI_MODE_NIGHT_YES;
+ configuration.uiMode &= ~Configuration.UI_MODE_NIGHT_NO;
+ } else {
+ configuration.uiMode |= Configuration.UI_MODE_NIGHT_NO;
+ configuration.uiMode &= ~Configuration.UI_MODE_NIGHT_YES;
+ }
+ }
+
+ super.attachBaseContext(context.createConfigurationContext(configuration));
+ return;
+ }
+ super.attachBaseContext(context);
+ }
+}
diff --git a/bin/drankcounter/src/nl/plaatsoft/drankcounter/activities/MainActivity.java b/bin/drankcounter/src/nl/plaatsoft/drankcounter/activities/MainActivity.java
new file mode 100644
index 00000000..e16ec076
--- /dev/null
+++ b/bin/drankcounter/src/nl/plaatsoft/drankcounter/activities/MainActivity.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (c) 2026 Bastiaan van der Plaat
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+package nl.plaatsoft.drankcounter.activities;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.ListView;
+import android.widget.PopupMenu;
+import android.widget.TextView;
+
+import nl.plaatsoft.android.alerts.RatingAlert;
+import nl.plaatsoft.android.alerts.UpdateAlert;
+import nl.plaatsoft.android.compat.ContextCompat;
+import nl.plaatsoft.android.fetch.FetchDataTask;
+import nl.plaatsoft.android.fetch.FetchImageTask;
+import nl.plaatsoft.drankcounter.DrinkDatabaseHelper;
+import nl.plaatsoft.drankcounter.R;
+import nl.plaatsoft.drankcounter.WidgetProvider;
+import nl.plaatsoft.drankcounter.components.DrinkAdapter;
+import nl.plaatsoft.drankcounter.models.Drink;
+
+import org.jspecify.annotations.Nullable;
+
+public class MainActivity extends BaseActivity implements PopupMenu.OnMenuItemClickListener {
+ private static final int SETTINGS_REQUEST_CODE = 1;
+
+ private Handler handler = new Handler(Looper.getMainLooper());
+ private int oldLanguage = -1;
+ private int oldTheme = -1;
+
+ private DrinkDatabaseHelper dbHelper;
+ private DrinkAdapter drinkAdapter;
+ private ListView drinkList;
+ private TextView emptyText;
+ private TextView beerCountView;
+ private TextView wineCountView;
+ private TextView liqueurCountView;
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ // Initialize database
+ dbHelper = new DrinkDatabaseHelper(this);
+
+ // Setup list with drink buttons as scrollable header
+ drinkList = findViewById(R.id.main_drink_list);
+ useWindowInsets(drinkList);
+ emptyText = findViewById(R.id.main_empty_list);
+
+ var headerView = LayoutInflater.from(this).inflate(R.layout.main_drink_header, drinkList, false);
+ drinkList.addHeaderView(headerView, null, false);
+
+ drinkAdapter = new DrinkAdapter(this, dbHelper);
+ drinkAdapter.setOnDeleteListener(this::refreshDrinkList);
+ drinkList.setAdapter(drinkAdapter);
+
+ beerCountView = headerView.findViewById(R.id.main_beer_count);
+ wineCountView = headerView.findViewById(R.id.main_wine_count);
+ liqueurCountView = headerView.findViewById(R.id.main_liqueur_count);
+
+ // Options menu button
+ findViewById(R.id.main_options_menu_button).setOnClickListener(view -> {
+ var optionsMenu = new PopupMenu(this, view, Gravity.TOP | Gravity.RIGHT);
+ optionsMenu.getMenuInflater().inflate(R.menu.options, optionsMenu.getMenu());
+ optionsMenu.setOnMenuItemClickListener(this);
+ optionsMenu.show();
+ });
+
+ // Add drink buttons (in scrollable header)
+ headerView.findViewById(R.id.main_add_beer_button).setOnClickListener(view -> addDrink(Drink.TYPE_BEER));
+ headerView.findViewById(R.id.main_add_wine_button).setOnClickListener(view -> addDrink(Drink.TYPE_WINE));
+ headerView.findViewById(R.id.main_add_liqueur_button).setOnClickListener(view -> addDrink(Drink.TYPE_LIQUEUR));
+
+ // Show update alert
+ UpdateAlert.checkAndShow(this,
+ "https://raw.githubusercontent.com/bplaat/android-apps/refs/heads/master/bin/drankcounter/bob.toml",
+ SettingsActivity.STORE_PAGE_URL);
+ }
+
+ private void addDrink(int type) {
+ dbHelper.insertDrink(type, System.currentTimeMillis() / 1000);
+ refreshDrinkList();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ refreshDrinkList();
+ }
+
+ private void refreshDrinkList() {
+ var drinks = dbHelper.getAllDrinks();
+ var todayDrinks = dbHelper.getTodaysDrinks();
+
+ emptyText.setVisibility(drinks.isEmpty() ? View.VISIBLE : View.GONE);
+ drinkAdapter.setDrinks(drinks);
+
+ beerCountView.setText(String.valueOf(DrinkDatabaseHelper.countByType(todayDrinks, Drink.TYPE_BEER)));
+ wineCountView.setText(String.valueOf(DrinkDatabaseHelper.countByType(todayDrinks, Drink.TYPE_WINE)));
+ liqueurCountView.setText(String.valueOf(DrinkDatabaseHelper.countByType(todayDrinks, Drink.TYPE_LIQUEUR)));
+
+ WidgetProvider.updateAllWidgets(this);
+ }
+
+ @Override
+ public boolean onMenuItemClick(@SuppressWarnings("null") MenuItem item) {
+ if (item.getItemId() == R.id.menu_options_settings) {
+ oldLanguage = settings.getLanguage();
+ oldTheme = settings.getTheme();
+ startActivityForResult(new Intent(this, SettingsActivity.class), SETTINGS_REQUEST_CODE);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, @SuppressWarnings("null") Intent data) {
+ // When settings activity is closed check for restart
+ if (requestCode == SETTINGS_REQUEST_CODE) {
+ if (oldLanguage != -1 && oldTheme != -1) {
+ if (oldLanguage != settings.getLanguage() || oldTheme != settings.getTheme()) {
+ handler.post(() -> recreate());
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (dbHelper != null) {
+ dbHelper.close();
+ }
+ }
+}
diff --git a/bin/drankcounter/src/nl/plaatsoft/drankcounter/activities/SettingsActivity.java b/bin/drankcounter/src/nl/plaatsoft/drankcounter/activities/SettingsActivity.java
new file mode 100644
index 00000000..bc6b3da3
--- /dev/null
+++ b/bin/drankcounter/src/nl/plaatsoft/drankcounter/activities/SettingsActivity.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (c) 2026 Bastiaan van der Plaat
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+package nl.plaatsoft.drankcounter.activities;
+
+import android.app.AlertDialog;
+import android.content.Intent;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import nl.plaatsoft.drankcounter.R;
+
+import org.jspecify.annotations.Nullable;
+
+@SuppressWarnings("this-escape")
+public class SettingsActivity extends BaseActivity {
+ public static final String STORE_PAGE_URL = "https://github.com/bplaat/android-apps/tree/master/bin/drankcounter";
+ private static final String ABOUT_WEBSITE_URL = "https://bplaat.nl/";
+
+ private int versionButtonClickCounter = 0;
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_settings);
+ useWindowInsets();
+
+ // Back button
+ findViewById(R.id.settings_back_button).setOnClickListener(view -> finish());
+
+ // Language button
+ var languages = new String[] {getResources().getString(R.string.settings_language_english),
+ getResources().getString(R.string.settings_language_dutch),
+ getResources().getString(R.string.settings_language_system)};
+ var language = settings.getLanguage();
+ ((TextView)findViewById(R.id.settings_language_label)).setText(languages[language]);
+ findViewById(R.id.settings_language_button).setOnClickListener(view -> {
+ var alertDialog = new AlertDialog.Builder(this)
+ .setTitle(R.string.settings_language_button)
+ .setSingleChoiceItems(languages, language,
+ (dialog, which) -> {
+ dialog.dismiss();
+ if (language != which) {
+ settings.setLanguage(which);
+ recreate();
+ }
+ })
+ .show();
+ var density = getResources().getDisplayMetrics().density;
+ alertDialog.getListView().setPadding(0, 0, 0, (int)(16 * density));
+ });
+
+ // Themes button
+ var themes = new String[] {getResources().getString(R.string.settings_theme_light),
+ getResources().getString(R.string.settings_theme_dark),
+ Build.VERSION.SDK_INT < Build.VERSION_CODES.Q
+ ? getResources().getString(R.string.settings_theme_battery_saver)
+ : getResources().getString(R.string.settings_theme_system)};
+ var theme = settings.getTheme();
+ ((TextView)findViewById(R.id.settings_theme_label)).setText(themes[theme]);
+ findViewById(R.id.settings_theme_button).setOnClickListener(view -> {
+ var alertDialog = new AlertDialog.Builder(this)
+ .setTitle(R.string.settings_theme_button)
+ .setSingleChoiceItems(themes, theme,
+ (dialog, which) -> {
+ dialog.dismiss();
+ if (theme != which) {
+ settings.setTheme(which);
+ recreate();
+ }
+ })
+ .show();
+ var density = getResources().getDisplayMetrics().density;
+ alertDialog.getListView().setPadding(0, 0, 0, (int)(16 * density));
+ });
+
+ // Version button easter egg
+ try {
+ ((TextView)findViewById(R.id.settings_version_label))
+ .setText("v" + getPackageManager().getPackageInfo(getPackageName(), 0).versionName);
+ } catch (NameNotFoundException exception) {
+ Log.e(getPackageName(), "Can't get app version", exception);
+ }
+ findViewById(R.id.settings_version_button).setOnClickListener(view -> {
+ versionButtonClickCounter++;
+ if (versionButtonClickCounter == 8) {
+ versionButtonClickCounter = 0;
+ Toast.makeText(this, R.string.settings_version_message, Toast.LENGTH_SHORT).show();
+ startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://youtu.be/dQw4w9WgXcQ?t=43")));
+ }
+ });
+
+ // Rate button
+ findViewById(R.id.settings_rate_button).setOnClickListener(view -> {
+ startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(STORE_PAGE_URL)));
+ });
+
+ // Share button
+ findViewById(R.id.settings_share_button).setOnClickListener(view -> {
+ var intent = new Intent();
+ intent.setAction(Intent.ACTION_SEND);
+ intent.setType("text/plain");
+ intent.putExtra(
+ Intent.EXTRA_TEXT, getResources().getString(R.string.settings_share_message, STORE_PAGE_URL));
+ startActivity(Intent.createChooser(intent, null));
+ });
+
+ // About button
+ findViewById(R.id.settings_about_button).setOnClickListener(view -> {
+ new AlertDialog.Builder(this)
+ .setTitle(R.string.settings_about_alert_title_label)
+ .setMessage(R.string.settings_about_alert_message_label)
+ .setNegativeButton(R.string.settings_about_alert_website_button,
+ (dialog, which) -> { startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(ABOUT_WEBSITE_URL))); })
+ .setPositiveButton(R.string.settings_about_alert_ok_button, null)
+ .show();
+ });
+
+ // Footer button
+ findViewById(R.id.settings_footer_button).setOnClickListener(view -> {
+ startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(ABOUT_WEBSITE_URL)));
+ });
+ }
+}
diff --git a/bin/drankcounter/src/nl/plaatsoft/drankcounter/components/DrinkAdapter.java b/bin/drankcounter/src/nl/plaatsoft/drankcounter/components/DrinkAdapter.java
new file mode 100644
index 00000000..3acf0d0a
--- /dev/null
+++ b/bin/drankcounter/src/nl/plaatsoft/drankcounter/components/DrinkAdapter.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright (c) 2026 Bastiaan van der Plaat
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+package nl.plaatsoft.drankcounter.components;
+
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+
+import android.content.Context;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.PopupMenu;
+import android.widget.TextView;
+
+import nl.plaatsoft.drankcounter.DrinkDatabaseHelper;
+import nl.plaatsoft.drankcounter.R;
+import nl.plaatsoft.drankcounter.Settings;
+import nl.plaatsoft.drankcounter.models.Drink;
+
+import org.jspecify.annotations.Nullable;
+
+public class DrinkAdapter extends BaseAdapter {
+ private static final int VIEW_TYPE_HEADER = 0;
+ private static final int VIEW_TYPE_DRINK = 1;
+
+ public interface OnDeleteListener {
+ void onDrinkDeleted();
+ }
+
+ private static class DrinkViewHolder {
+ public @SuppressWarnings("null") ImageView drinkIcon;
+ public @SuppressWarnings("null") TextView drinkType;
+ public @SuppressWarnings("null") TextView drinkTime;
+ public @SuppressWarnings("null") ImageButton drinkMenuButton;
+ }
+
+ private final Context context;
+ private final DrinkDatabaseHelper dbHelper;
+ private final SimpleDateFormat dayKeyFormat;
+ private final SimpleDateFormat dayDisplayFormat;
+ private final SimpleDateFormat timeFormat;
+ private final String todayLabel;
+ private final String yesterdayLabel;
+ private final List |