Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
67c9888
Initial commit
darajahabib24-creator Oct 22, 2026
88b3423
Merge remote-tracking branch 'origin/main'
darajahabib24-creator Oct 22, 2026
f31d5c5
Add Kivy launcher project
darajahabib24-creator Oct 22, 2026
c66e311
Fix launcher startup and update Android APK
darajahabib24-creator Oct 22, 2026
3df3311
Delete kivy-launcher-project.zip
darajahabib24-creator Oct 22, 2026
c396130
Update CI workflow for Android APK build
darajahabib24-creator Oct 22, 2026
f4a5414
Delete kivy-launcher directory
darajahabib24-creator Oct 22, 2026
26f7f90
Add GitHub Actions workflow to build Android APK
darajahabib24-creator Oct 22, 2026
d7d1da5
Create a basic Kivy application with a button
darajahabib24-creator Oct 22, 2026
e7d18b4
Add buildozer.spec for Kivy app configuration
darajahabib24-creator Oct 22, 2026
3e054f1
Add .gitignore for Python and Buildozer files
darajahabib24-creator Oct 22, 2026
59d1583
Fix typo in Python version setup
darajahabib24-creator Oct 22, 2026
715c3d4
Move APK build workflow to .github/workflows and update runner
darajahabib24-creator Oct 28, 2026
c60d869
Add KivyMD, ffpyplayer/ffmpeg, internet + storage-manager permissions
darajahabib24-creator Oct 28, 2026
98c4e8f
Pin python-for-android to ffmpeg 6.1.2 commit to fix ffpyplayer build
darajahabib24-creator Oct 28, 2026
6df3df2
Use NDK r25b: ffmpeg 6.1.2 vulkan code fails on r28c clang
darajahabib24-creator Oct 28, 2026
c23ac64
Pin p4a to Python 3.11/ffmpeg 6.1.2 stack (3.14 default has broken ho…
darajahabib24-creator Oct 28, 2026
ed5d0fb
Bust stale buildozer cache: stop mixing toolchain artifacts across runs
darajahabib24-creator Oct 28, 2026
96e1ecd
Install libtool 2.4.6 in CI: LT_SYS_SYMBOL_USCORE missing on 2.5
darajahabib24-creator Oct 28, 2026
dd0890b
CI: pin autoconf 2.69 — autoconf 2.72 breaks libffi's autogen.sh (LT_…
darajahabib24-creator Oct 28, 2026
b094316
CI: install libltdl-dev — fixes libffi autogen.sh (LT_SYS_SYMBOL_USCO…
darajahabib24-creator Oct 28, 2026
0e29d69
CI: cache only ~/.buildozer and bust key — poisoned .buildozer tree c…
darajahabib24-creator Oct 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
name: Build Android APK

on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
workflow_dispatch: # Allows you to manually trigger the build from the GitHub UI

jobs:
build:
runs-on: ubuntu-24.04

steps:
- name: Checkout Code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'

# Caching Buildozer's global dir (SDK/NDK) drastically reduces build times.
# IMPORTANT:
# * do NOT cache the project-local .buildozer build tree — a build tree
# saved from a failed run poisons later runs (e.g. hostpython3 with a
# missing 'math' module, because its should_build() skips rebuilding)
# * no restore-keys — restoring older artifacts mixes toolchain versions
- name: Cache Buildozer Android SDK/NDK
uses: actions/cache@v4
with:
path: ~/.buildozer
key: ${{ runner.os }}-buildozer-v4-${{ hashFiles('buildozer.spec') }}

- name: Install System Dependencies
run: |
sudo apt update
sudo apt install -y \
autoconf build-essential ccache cmake git libffi-dev \
libncurses-dev libssl-dev libtinfo6 libtool pkg-config \
unzip zlib1g-dev zip

# The pinned python-for-android's libffi recipe runs 'autogen.sh'
# (autoreconf) and needs the LT_SYS_SYMBOL_USCORE macro. Without
# 'libltdl-dev' (which puts libtool's m4 on aclocal's search path)
# autoreconf fails with "possibly undefined macro: LT_SYS_SYMBOL_USCORE".
- name: Install libltdl-dev
run: |
sudo apt-get update
sudo apt-get install -y libltdl-dev

- name: Install Python Dependencies & Buildozer
run: |
python -m pip install --upgrade pip setuptools wheel
python -m pip install --upgrade buildozer Cython Kivy

- name: Build Android Debug APK
run: |
# The 'yes |' automatically accepts the Android SDK license prompts
yes | buildozer android debug

- name: Upload APK Artifact
uses: actions/upload-artifact@v4
with:
name: Kivy-Android-Debug-APK
path: bin/*.apk
if-no-files-found: error
12 changes: 12 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Python caches
__pycache__/
*.pyc

# Buildozer environment and cache directories
.buildozer/
bin/
.venv/
ENV/

# OS specific hidden files
.DS_Store
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# My Kivy App

A simple Android app built with [Kivy](https://kivy.org) and
[KivyMD](https://kivymd.readthedocs.io) — a Material Design welcome screen with
buttons to test internet access, file storage, and the ffpyplayer/ffmpeg
libraries.

## Permissions (in `buildozer.spec`)

- `INTERNET` — connectivity test
- `READ_EXTERNAL_STORAGE` / `WRITE_EXTERNAL_STORAGE`
- `MANAGE_EXTERNAL_STORAGE` — **All files access** storage manager
(grant via the in-app "Manage Storage" button, opened from system settings)

## Build the APK

The APK is built automatically by GitHub Actions on every push to `main`
(see [`.github/workflows/build.yml`](.github/workflows/build.yml)).

To download the latest APK:

1. Open the repo's **Actions** tab and click the latest **Build Android APK** run.
2. Download the **Kivy-Android-Debug-APK** artifact and install it on your phone.

## Run locally (desktop)

```bash
pip install kivy
python main.py
```

## Build locally (Android)

```bash
pip install buildozer
buildozer android debug
```

The generated APK is written to `bin/`.
70 changes: 70 additions & 0 deletions buildozer.spec
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
[app]
# (str) Title of your application
title = My Kivy App

# (str) Package name
package.name = mykivyapp

# (str) Package domain (needed for android packaging)
package.domain = org.example

# (str) Source code where the main.py lives
source.dir = .

# (list) Source files to include (let empty to include all the files)
source.include_exts = py,png,jpg,kv,atlas

# (str) Application versioning
version = 0.2

# (list) Application requirements
# comma separated e.g. requirements = sqlite3,kivy
# - kivymd : Material Design widgets (KivyMD)
# - ffpyplayer : pulls in the ffmpeg + SDL2 recipes automatically
# - SDL2 / OpenGL ES 2 (and 3 on supported devices) come from the 'sdl2' bootstrap below
# - pyjnius (Java <-> Python bridge) is added automatically by the android bootstrap
requirements = python3,kivy,kivymd,ffpyplayer

# (str) Pin python-for-android to a commit that ships a known-good
# build stack: Python 3.11.5 + ffmpeg 6.1.2 + ffpyplayer 4.5.1.
# - Newer p4a defaults Python to 3.14.0, whose host build is broken
# ("No module named 'math'"), and
# - ffmpeg 8.x (p4a commit e564e44d, Jan 2026) removed
# 'libavcodec/avfft.h', breaking ffpyplayer (p4a issue #3344).
p4a.commit = c02cf781a0d3d232eb4ffbeaf580cc82cf2bd65f

# (list) Supported orientations
orientation = portrait

# -----------------------------------------------------------------------------
# Android specific configuration
# -----------------------------------------------------------------------------

# (int) Target Android API, should be as high as possible.
android.api = 34

# (int) Minimum API your APK will support.
android.minapi = 21

# (str) Android NDK version. r25b (clang 14) is required for the pinned
# ffmpeg 6.1.2: newer NDKs use clang CFLAGS that turn the Vulkan callback
# warning in libavutil/hwcontext_vulkan.c into a hard error.
android.ndk = 25b

# (list) App permissions
# - INTERNET and READ/WRITE_EXTERNAL_STORAGE
# - MANAGE_EXTERNAL_STORAGE = "All files access" storage manager (granted via
# system settings; main.py opens that screen through pyjnius)
android.permissions = INTERNET,READ_EXTERNAL_STORAGE,WRITE_EXTERNAL_STORAGE,MANAGE_EXTERNAL_STORAGE

# (list) The Android architectures to build for
android.archs = armeabi-v7a

# (str) Android logcat filters to use
android.logcat_filters = *:S python:D

# (bool) Copy library instead of making a libpymodules.so
android.copy_libs = 1

# (str) The bootstrap to use for android builds
android.bootstrap = sdl2
153 changes: 153 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
"""
My Kivy App — KivyMD + storage manager demo.

- KivyMD Material Design UI
- INTERNET permission (connectivity test)
- READ/WRITE_EXTERNAL_STORAGE (write test to Downloads)
- MANAGE_EXTERNAL_STORAGE ("All files access" storage manager, opened
from system settings through pyjnius)
- ffpyplayer / ffmpeg availability check (pulled in by buildozer.spec)
"""

import os

import kivy
from kivy.uix.boxlayout import BoxLayout

from kivymd.app import MDApp
from kivymd.uix.button import MDRaisedButton
from kivymd.uix.label import MDLabel


def is_android():
return 'ANDROID_ROOT' in os.environ or os.path.exists('/storage/emulated/0')


def downloads_dir():
"""Directory used for the storage write test."""
if is_android():
try:
from android.storage import primary_external_storage_path
return os.path.join(primary_external_storage_path(), 'Download')
except Exception:
return '/storage/emulated/0/Download'
return os.path.expanduser('~/Downloads')


class MainApp(MDApp):
def build(self):
self.theme_cls.primary_palette = 'Blue'
self.title = 'My Kivy App'

layout = BoxLayout(
orientation='vertical', padding=20, spacing=12
)

welcome = MDLabel(
text='Welcome to your Kivy App!',
halign='center',
font_style='H5',
)

# Shows versions / environment info once checked
self.status = MDLabel(
text='Tap a button to run a test.',
halign='center',
theme_text_color='Secondary',
)

self.add_button(layout, 'Check Libraries', self.check_libraries)
self.add_button(layout, 'Test Internet', self.check_internet)
self.add_button(layout, 'Test Storage (write file)', self.test_storage)
self.add_button(layout, 'Manage Storage (All files access)',
self.open_all_files_settings)

layout.add_widget(welcome)
layout.add_widget(self.status)
return layout

def add_button(self, layout, text, callback):
btn = MDRaisedButton(text=text, size_hint=(1, None),
height='48dp', on_press=callback)
layout.add_widget(btn)

def set_status(self, msg):
self.status.text = msg

# ------------------------------------------------------------------ #
# Button actions
# ------------------------------------------------------------------ #

def check_libraries(self, *args):
"""Verify ffpyplayer (ffmpeg backend) and the SDL2/OpenGL ES window.
Running this at all on Android means SDL2 + OpenGL ES initialized."""
import ffpyplayer
from kivy.core.window import Window # noqa: F401 (forces GL init)

self.set_status('Kivy {} ✓ | ffpyplayer {} ✓ (ffmpeg) | '
'SDL2 + OpenGL ES ✓'.format(
kivy.__version__, ffpyplayer.__version__))

def check_internet(self, *args):
"""Quick connectivity test (needs the INTERNET permission)."""
import threading

def run():
try:
from urllib import request
with request.urlopen('https://kivy.org', timeout=5) as resp:
msg = 'Internet OK (HTTP {})'.format(resp.status)
except Exception as e:
msg = 'Internet failed: {}'.format(e)
self.set_status(msg)

self.set_status('Checking internet…')
threading.Thread(target=run, daemon=True).start()

def test_storage(self, *args):
"""Write a test file to the shared Downloads folder."""
target = os.path.join(downloads_dir(), 'mykivyapp_test.txt')
try:
os.makedirs(downloads_dir(), exist_ok=True)
with open(target, 'w') as f:
f.write('Hello from My Kivy App!\n')
self.set_status('Storage OK: wrote {}'.format(target))
except PermissionError:
self.set_status('Storage denied — use "Manage Storage" to '
'grant all-files access, then try again.')
except Exception as e:
self.set_status('Storage failed: {}'.format(e))

def open_all_files_settings(self, *args):
"""Open the system 'All files access' (MANAGE_EXTERNAL_STORAGE)
settings screen for this app via pyjnius."""
if not is_android():
self.set_status('"All files access" is Android-only.')
return
try:
from jnius import autoclass

Settings = autoclass('android.provider.Settings')
Environment = autoclass('android.os.Environment')
Intent = autoclass('android.content.Intent')
Uri = autoclass('android.net.Uri')
PythonActivity = autoclass('org.kivy.android.PythonActivity')

if Environment.isExternalStorageManager():
self.set_status('All-files access already granted ✓')
return

package = PythonActivity.mPackageName
intent = Intent(
Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION,
Uri.parse('package:' + package),
)
PythonActivity.mActivity.startActivity(intent)
self.set_status('Opened storage settings — grant access, then '
'run the storage test.')
except Exception as e:
self.set_status('Could not open storage settings: {}'.format(e))


if __name__ == '__main__':
MainApp().run()