Skip to content
Fabian edited this page Dec 30, 2025 · 5 revisions

How to Create Android Widget

step 1: First you Design How you want the Widget to Look [it's Layout].

Store it in: res/layout/simple_widget.xml This a simple widget with a text

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:padding="10dp"
    android:background="#FFFFFF"
    android:gravity="center"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/widget_text"
        android:text="Loading..."
        android:textSize="18sp"
        android:textColor="#000"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

</LinearLayout>

step 2: Create an xml containing the info about the widget.

Like: size, preview icon and others
path: res/xml/widgetproviderinfo.xml

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="120dp"
    android:minHeight="60dp"
    android:updatePeriodMillis="1800000"
    android:initialLayout="@layout/simple_widget"
    android:previewImage="@drawable/ic_launcher_foreground"
    android:resizeMode="horizontal|vertical"
    android:widgetCategory="home_screen">
</appwidget-provider>

Create preview image png in right pathres/drawable/ic_launcher_foreground.png

step 3: Create a AppWidgetProvider it's used to receive events for widget.

path: src/SimpleWidget.java.
This will receive an event when widget is add to change it's text

package org.wally.waller; // Change here from buildozer.spec package.domain+package.name

import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.widget.RemoteViews;

import org.wally.waller.R; // Change here from buildozer.spec package.domain+package.name
import android.app.PendingIntent;
import android.content.Intent;

public class SimpleWidget extends AppWidgetProvider {

    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        for (int appWidgetId : appWidgetIds) {

            RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.simple_widget);

            // Example: Set text
            views.setTextViewText(R.id.widget_text, "Hello Widget!");

            // Update widget
            appWidgetManager.updateAppWidget(appWidgetId, views);
        }
    }
}

step 4: Automate injecting Receiver in XML

path:p4a/hook.py

from pathlib import Path
from pythonforandroid.toolchain import ToolchainCL


def after_apk_build(toolchain: ToolchainCL):
    manifest_file = Path(toolchain._dist.dist_dir) / "src" / "main" / "AndroidManifest.xml"
    text = manifest_file.read_text(encoding="utf-8")

    package = "org.wally.waller" # Change here from buildozer.spec package.domain+package.name

    receiver_xml = f'''
    <receiver android:name="{package}.SimpleWidget"
              android:enabled="true"
              android:exported="false">
        <intent-filter>
            <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
        </intent-filter>
        <meta-data android:name="android.appwidget.provider"
               android:resource="@xml/widgetproviderinfo" />
    </receiver>
    '''

    if receiver_xml.strip() not in text:
        if "</application>" in text:
            text = text.replace("</application>", f"{receiver_xml}\n</application>")
            print("Receiver added")
        else: 
            print("Could not find </application> to insert receiver")
    else: 
        print("Receiver already exists in manifest")

    manifest_file.write_text(text, encoding="utf-8")
    print("Successfully_101: Manifest update completed successfully!")

step 5: From buildozer.spec tell it you want to add resources, src and p4a hook

android.add_resources = res
android.add_src = src
p4a.hook = p4a/hook.py

sample image


Creating a foreground service

Things you'll need

  • For Android 14+, To choose a foreground service type from android docs

Steps

1. Specify service in buildozer.spec Properly

According to p4a docs we need to specify it's going to be a foreground service in buildozer.spec

services = Mydownloader:./services/mydownloader.py:foreground

# my package for notifications
requirements = python3, kivy, pyjnius, android-notify==1.60.5.dev0

# Add permission for notifications
android.permissions = POST_NOTIFICATIONS

2. Automate adding Service Type in AndroidManifest.xml

To automate this process from your buildozer.spec by passing in an argument for p4a.hook

p4a.hook = p4a/hook.py

You have to use foreground service type fitting your use-case.

For this example I'm using dataSync which is for download|uploads.

Programmatically found my service and added a type argument:

# p4a/hook.py
from pathlib import Path
from pythonforandroid.toolchain import ToolchainCL

def after_apk_build(toolchain: ToolchainCL):
    manifest_file = Path(toolchain._dist.dist_dir) / "src" / "main" / "AndroidManifest.xml"
    text = manifest_file.read_text(encoding="utf-8")

    # Change these three lines to fit your use-case 
    package = "org.laner.lan_ft" # Find value in your buildozer.spec "package.domain.package.name"
    service_name="Mydownloader"
    foreground_type="dataSync"
    target = f'android:name="{package}.Service{service_name.capitalize()}"'

    # Inject foregroundServiceType
    pos = text.find(target)
    if pos != -1:
        end = text.find("/>", pos)
        text = (
                    text[:end] +
                    ' android:foregroundServiceType="{foreground_type}"' +
                    text[end:]
                )
        print("Successfully Added foregroundServiceType to ServiceMydownloader")

    text = text.replace("</application>", f"{receiver_xml}\n</application>")
    print("Successfully added Receiver")

    # Write back the final manifest
    manifest_file.write_text(text, encoding="utf-8")
    print("Manifest update completed",text)

3. Foreground Permissions

  • You must add FOREGROUND_SERVICE and POST_NOTIFICATIONS
  • According to your foreground type you might have an additional permission in my case only FOREGROUND_SERVICE_DATA_SYNC
android.permissions = FOREGROUND_SERVICE, POST_NOTIFICATIONS, FOREGROUND_SERVICE_DATA_SYNC

4. Start your service then promote it to Foreground Service

# main.py
def start_service():
    from android import mActivity
    from jnius import autoclass

    context = mActivity.getApplicationContext()
    service_name="Mydownloader"
    service_class=autoclass(context.getPackageName() + '.Service' + service_name.capitalize())
    service.start(self.mActivity, "port number")

Finally in your service file Promote it to a foreground service

Reduced boilerplate with android-notify

# ./services/mydownloader.py
print("Entered Service File...")
import time,os
from jnius import autoclass
from android_notify import Notification

# get and see parameter from ui 
Notification(title = os.environ.get('PYTHON_SERVICE_ARGUMENT', 'no arg')).send()

BuildVersion = autoclass("android.os.Build$VERSION")
ServiceInfo = autoclass("android.content.pm.ServiceInfo")
PythonService = autoclass('org.kivy.android.PythonService')

service = PythonService.mService
foreground_type= ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC if BuildVersion.SDK_INT >= 30 else 0
fmt = lambda s: f"{int(s//3600)}h {int((s%3600)//60)}m {int(s%60)}s"


n=Notification(title="Foreground Service Active", message="This service is running in the foreground")
builder=n.start_building() # not using .send() allowing .startForeground() to send initial notification 
service.startForeground(n.id, builder.build(), foreground_type)
service.setAutoRestartService(True)  # auto-restart if killed


print("Foreground Service is alive. Entering main loop...")
n1 = Notification(title="Running for 0h 0m 0s")
n1.send()

start = time.time()
while True:
    elapsed = time.time() - start
    n1.updateTitle(f"Running for {fmt(elapsed)}")
    time.sleep(2)