Metadata-Version: 2.4
Name: flet-android-notifications
Version: 0.5.2
Summary: Native local notifications for Flet apps (Android)
Project-URL: Homepage, https://github.com/alex-stoica/flet-android-notifications
Project-URL: Repository, https://github.com/alex-stoica/flet-android-notifications
Project-URL: Issues, https://github.com/alex-stoica/flet-android-notifications/issues
Author-email: Alex Stoica <alexstoica@protonmail.com>
License-Expression: MIT
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Requires-Dist: flet>=0.80.5
Description-Content-Type: text/markdown

# flet-android-notifications

Native Android notifications for Flet apps. Wraps the `flutter_local_notifications` Flutter plugin through a custom Flet extension, giving your Python code full access to Android's notification system.

Flet has no built-in support for native notifications, and every obvious Python approach (plyer, Pyjnius, android-notify) fails because Flet's Python process is sandboxed from Android APIs. This extension solves that by bridging Python to Dart to the Flutter plugin.

## Features

- show, cancel, and cancel all notifications
- schedule notifications for a future time (one-shot or recurring via AlarmManager)
- notification action buttons (e.g. "Approve" / "Deny") with per-button callbacks
- configurable notification channels (id, name, description, importance, sound, vibration)
- tap and action callbacks with payload support
- notification styles: big text, big picture, and inbox (expandable rich notifications)
- progress bar notifications (determinate and indeterminate)
- permission handling for Android 13+ and exact alarm permission for Android 14+
- proper error propagation via `NotificationError`

## Install

```bash
pip install flet-android-notifications
# or
uv add flet-android-notifications
```

In your app's `pyproject.toml`, declare the Android permission and tell Flet where to find the extension for APK builds:

```toml
[project]
dependencies = [
    "flet>=0.80.5",
    "flet-android-notifications",
]

[tool.flet.android.permission]
"android.permission.POST_NOTIFICATIONS" = true
# Required for scheduled notifications:
"android.permission.SCHEDULE_EXACT_ALARM" = true
"android.permission.RECEIVE_BOOT_COMPLETED" = true

[tool.flet.app]
exclude = ["flet_android_notifications"]

[tool.flet.dev_packages]
flet-android-notifications = "flet_android_notifications"
```

The `exclude` line prevents the extension source from being raw-copied into the APK, which would shadow the installed package and break imports.

## Usage

```python
import json
from datetime import datetime, timedelta
import flet as ft
from flet_android_notifications import FletAndroidNotifications, NotificationError


def main(page: ft.Page):

    def on_notification_tap(e):
        data = json.loads(e.data)
        payload = data.get("payload", "")
        action_id = data.get("action_id", "")
        print(f"tapped: payload={payload}, action={action_id}")

    notifications = FletAndroidNotifications(
        on_notification_tap=on_notification_tap,
    )

    async def send_now(e):
        await notifications.request_permissions()
        await notifications.show_notification(
            notification_id=1,
            title="Hello",
            body="This is an instant notification.",
        )

    async def send_with_actions(e):
        await notifications.request_permissions()
        await notifications.show_notification(
            notification_id=2,
            title="Review request",
            body="You have a task to review.",
            payload="task_42",
            actions=[
                {"id": "approve", "title": "Approve"},
                {"id": "deny", "title": "Deny"},
            ],
        )

    async def schedule_30s(e):
        await notifications.request_permissions()
        await notifications.schedule_notification(
            notification_id=10,
            title="Reminder",
            body="This fired 30 seconds after you pressed the button.",
            scheduled_time=datetime.now() + timedelta(seconds=30),
        )

    page.add(
        ft.Column([
            ft.Button(content="Send now", on_click=send_now),
            ft.Button(content="Send with actions", on_click=send_with_actions),
            ft.Button(content="Schedule in 30s", on_click=schedule_30s),
        ])
    )


ft.run(main)
```

Just instantiate `FletAndroidNotifications`. Do not add it to `page.overlay` or `page.controls` -- it's a service, not a visual control, and it registers itself automatically.

See the [`examples/`](examples/) folder for more: [`simple.py`](examples/simple.py), [`action_buttons.py`](examples/action_buttons.py), [`scheduled.py`](examples/scheduled.py), [`big_text.py`](examples/big_text.py), [`notification_styles.py`](examples/notification_styles.py), [`advanced_options.py`](examples/advanced_options.py).

## API

### `FletAndroidNotifications(on_notification_tap=callback)`

The service. Instantiate once. The `on_notification_tap` callback receives an event where `e.data` is a JSON string:

```json
{"payload": "task_42", "action_id": "approve"}
```

`action_id` is an empty string `""` when the user taps the notification body rather than an action button.

### `await show_notification(...)`

| Parameter | Type | Default | Description |
|---|---|---|---|
| `notification_id` | `int` | required | unique id for this notification |
| `title` | `str` | required | notification title |
| `body` | `str` | required | notification body text |
| `payload` | `str` | `""` | arbitrary string returned in tap callback |
| `actions` | `list[dict]` | `None` | action buttons, each `{"id": "...", "title": "..."}` |
| `channel_id` | `str` | `"flet_notifications"` | Android notification channel id |
| `channel_name` | `str` | `"Flet Notifications"` | channel name shown in system settings |
| `channel_description` | `str` | `"Notifications from Flet app"` | channel description |
| `importance` | `str` | `"high"` | one of `none`, `min`, `low`, `default`, `high`, `max` |
| `play_sound` | `bool` | `True` | play default notification sound |
| `enable_vibration` | `bool` | `True` | vibrate on notification |
| `style` | `BigTextStyle\|BigPictureStyle\|InboxStyle\|None` | `None` | rich notification style |
| `show_progress` | `bool` | `False` | show a progress bar |
| `max_progress` | `int` | `0` | maximum progress value |
| `progress` | `int` | `0` | current progress value |
| `indeterminate` | `bool` | `False` | indeterminate progress bar |
| `group_key` | `str\|None` | `None` | group key for bundling notifications together |
| `set_as_group_summary` | `bool` | `False` | if True, this is the group summary notification |
| `group_alert_behavior` | `str` | `"all"` | `"all"`, `"summary"`, or `"children"` |
| `icon` | `str\|None` | `None` | drawable resource name for small icon (e.g. `"ic_notification"`). None = app launcher icon. Android renders small icons as single-color silhouettes. |
| `large_icon` | `str\|None` | `None` | large icon shown as thumbnail on right side |
| `large_icon_type` | `str` | `"drawable_resource"` | `"drawable_resource"` or `"file_path"` |
| `color` | `str\|None` | `None` | hex color (e.g. `"#FF5722"`) for accent color / small icon tint. See [Samsung color note](#samsung-oneui-notes). |
| `colorized` | `bool` | `False` | apply color as background (foreground service / media-style only) |
| `sound` | `str\|None` | `None` | raw resource name (e.g. `"alert_tone"` for `res/raw/alert_tone.mp3`). Sound is permanently bound to the channel — changing it requires a different `channel_id`. |
| `ongoing` | `bool` | `False` | persistent notification that can't be swiped away |
| `auto_cancel` | `bool` | `True` | dismiss notification when tapped |
| `silent` | `bool` | `False` | suppress sound and vibration |
| `only_alert_once` | `bool` | `False` | only alert (sound/vibration) on first show; updates are silent |
| `visibility` | `str\|None` | `None` | lock screen visibility: `"public"`, `"private"`, or `"secret"` |
| `sub_text` | `str\|None` | `None` | small text shown below the notification content |
| `channel_bypass_dnd` | `bool` | `False` | allow channel to bypass do-not-disturb (only takes effect when channel is first created) |
| `vibration_pattern` | `list[int]\|None` | `None` | custom vibration pattern in ms, e.g. `[0, 500, 200, 500]` |

Raises `NotificationError` on failure.

### `await schedule_notification(...)`

| Parameter | Type | Default | Description |
|---|---|---|---|
| `notification_id` | `int` | required | unique id for this notification |
| `title` | `str` | required | notification title |
| `body` | `str` | required | notification body text |
| `scheduled_time` | `datetime` | required | when to fire; naive = local time, aware = converted to UTC |
| `payload` | `str` | `""` | arbitrary string returned in tap callback |
| `actions` | `list[dict]` | `None` | action buttons, each `{"id": "...", "title": "..."}` |
| `channel_id` | `str` | `"flet_notifications"` | Android notification channel id |
| `channel_name` | `str` | `"Flet Notifications"` | channel name shown in system settings |
| `channel_description` | `str` | `"Notifications from Flet app"` | channel description |
| `importance` | `str` | `"high"` | one of `none`, `min`, `low`, `default`, `high`, `max` |
| `play_sound` | `bool` | `True` | play default notification sound |
| `enable_vibration` | `bool` | `True` | vibrate on notification |
| `schedule_mode` | `str` | `"inexact_allow_while_idle"` | how Android schedules the alarm (see table below) |
| `match_date_time_components` | `str\|None` | `None` | for recurring: `"time"` (daily), `"day_of_week_and_time"` (weekly), `"day_of_month_and_time"` (monthly), `"date_and_time"` (yearly), or `None` (one-shot) |
| `style` | `BigTextStyle\|BigPictureStyle\|InboxStyle\|None` | `None` | rich notification style |
| `show_progress` | `bool` | `False` | show a progress bar |
| `max_progress` | `int` | `0` | maximum progress value |
| `progress` | `int` | `0` | current progress value |
| `indeterminate` | `bool` | `False` | indeterminate progress bar |
| `group_key` | `str\|None` | `None` | group key for bundling notifications together |
| `set_as_group_summary` | `bool` | `False` | if True, this is the group summary notification |
| `group_alert_behavior` | `str` | `"all"` | `"all"`, `"summary"`, or `"children"` |
| `icon` | `str\|None` | `None` | drawable resource name for small icon |
| `large_icon` | `str\|None` | `None` | large icon thumbnail on right side |
| `large_icon_type` | `str` | `"drawable_resource"` | `"drawable_resource"` or `"file_path"` |
| `color` | `str\|None` | `None` | hex color for accent / small icon tint |
| `colorized` | `bool` | `False` | apply color as background (foreground service only) |
| `sound` | `str\|None` | `None` | raw resource name for custom sound |
| `ongoing` | `bool` | `False` | persistent notification that can't be swiped away |
| `auto_cancel` | `bool` | `True` | dismiss notification when tapped |
| `silent` | `bool` | `False` | suppress sound and vibration |
| `only_alert_once` | `bool` | `False` | only alert on first show; updates are silent |
| `visibility` | `str\|None` | `None` | lock screen visibility: `"public"`, `"private"`, or `"secret"` |
| `sub_text` | `str\|None` | `None` | small text shown below the notification content |
| `channel_bypass_dnd` | `bool` | `False` | allow channel to bypass do-not-disturb |
| `vibration_pattern` | `list[int]\|None` | `None` | custom vibration pattern in ms, e.g. `[0, 500, 200, 500]` |

Raises `NotificationError` on failure.

#### Schedule modes

| Mode | Needs `SCHEDULE_EXACT_ALARM`? | Fires during Doze? | Notes |
|---|---|---|---|
| `"inexact"` | No | No | Battery-friendly, may be deferred |
| `"inexact_allow_while_idle"` | No | Yes | Safe default, no special permission needed |
| `"exact"` | Yes | No | Exact time, but deferred during Doze |
| `"exact_allow_while_idle"` | Yes | Yes | Exact time, fires even in Doze |
| `"alarm_clock"` | Yes | Yes | Shows alarm icon in status bar |

#### Notification styles

Use style classes to create rich expandable notifications:

```python
from flet_android_notifications import BigTextStyle, BigPictureStyle, InboxStyle

# big text — expands to show longer content
await notifications.show_notification(
    notification_id=1, title="Report", body="Summary ready.",
    style=BigTextStyle("Full detailed report text here...",
                       content_title="Report — expanded",
                       summary_text="3 items"),
)

# big picture — shows an image when expanded
await notifications.show_notification(
    notification_id=2, title="Photo", body="New photo.",
    style=BigPictureStyle(drawable_resource="ic_launcher_foreground"),
)

# inbox — shows a list of lines when expanded
await notifications.show_notification(
    notification_id=3, title="3 messages", body="You have mail.",
    style=InboxStyle(["Alice: Hi", "Bob: Done", "Carol: OK"],
                     summary_text="from 3 contacts"),
)

# progress bar
await notifications.show_notification(
    notification_id=4, title="Uploading", body="45%",
    show_progress=True, max_progress=100, progress=45,
)
```

#### Advanced options

Use `ongoing` and `silent` together for a persistent download tracker that doesn't interrupt the user:

```python
await notifications.show_notification(
    notification_id=10, title="Downloading", body="45%",
    ongoing=True, silent=True,
    show_progress=True, max_progress=100, progress=45,
)
```

Use `visibility` to hide sensitive content on the lock screen:

```python
await notifications.show_notification(
    notification_id=11, title="New message", body="Contents hidden.",
    visibility="secret",
)
```

### `await request_exact_alarm_permission()`

Request the `SCHEDULE_EXACT_ALARM` permission (required on Android 14+ for exact schedule modes). Returns `True` if granted, `False` if denied. Inexact modes do not need this permission.

### `await cancel(notification_id)`

Cancel a specific notification by id.

### `await cancel_all()`

Cancel all active notifications.

### `await request_permissions()`

Request the POST_NOTIFICATIONS runtime permission (required on Android 13+). Returns `True` if granted, `False` if denied.

## Building the APK

```bash
# first build -- generates Flutter template, will likely fail at Gradle
flet build apk -v

# patch desugaring into build/flutter/android/app/build.gradle.kts:
#   android { compileOptions { isCoreLibraryDesugaringEnabled = true } }
#   dependencies { coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4") }

# rebuild -- the patch survives because the template hash is unchanged
flet build apk -v
```

The desugaring patch is needed because `flutter_local_notifications` v19+ uses Java 8 APIs that aren't available on older Android versions without it. You only need to apply this once per clean build directory.

### AndroidManifest.xml setup for scheduled notifications

If you use `schedule_notification()`, you must register the plugin's BroadcastReceivers so that scheduled notifications survive app restarts and device reboots. After your first `flet build apk`, add the following inside the `<application>` tag in `build/flutter/android/app/src/main/AndroidManifest.xml`:

```xml
<receiver android:exported="false"
    android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" />
<receiver android:exported="false"
    android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />
        <action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
        <action android:name="android.intent.action.QUICKBOOT_POWERON" />
        <action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
    </intent-filter>
</receiver>
```

Without these receivers, scheduled notifications will fire while the app is running but will be lost if the app is killed or the device restarts.

On Windows, set `PYTHONIOENCODING=utf-8` before building to avoid Unicode crashes from Rich's spinner characters.

### Installing on device

Always do a full uninstall before installing a new APK. Flet's `serious_python` caches the extracted Python environment and won't pick up code changes with `adb install -r`:

```bash
adb uninstall com.yourapp.package
adb install build/apk/app-release.apk
```

## Samsung OneUI notes

Samsung OneUI overrides some standard Android notification behaviors:

- **Color**: Samsung's system color palette overrides the programmatic `color` parameter. The accent color / small icon tint works on stock Android (Pixel, AOSP) but is ignored on Samsung. Users can disable this in Settings > Wallpaper and style > Color palette, or via Good Lock's QuickStar module.
- **Brief mode**: Samsung's default compact notification view hides expanded content (large icons, styles, color). Users must swipe down on a notification to see the full expanded view.
- **`colorized`**: only works for foreground service and media-style notifications on all Android devices, not just Samsung.

These are OEM-level behaviors and cannot be overridden from app code.

## Custom resources (icons, sounds)

To use custom small icons or notification sounds, place the resource files in your Android `res/` directory:

- **Small icons**: add a vector drawable XML to `res/drawable/` (24dp, white on transparent). Android renders small icons as single-color silhouettes.
- **Sounds**: add audio files (WAV, MP3, OGG) to `res/raw/`. Reference by name without extension: `sound="alert_tone"` for `res/raw/alert_tone.mp3`.

**Important**: resources only referenced at runtime (via `icon="..."` or `sound="..."`) may be stripped by Android's resource optimizer. Add a `res/raw/keep.xml` file to prevent this:

```xml
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:keep="@raw/*,@drawable/ic_*" />
```

Sound is permanently bound to a notification channel at creation. To change the sound, use a different `channel_id` or uninstall the app to reset all channels.

## Limitations

- Android only. The extension wraps `flutter_local_notifications` which supports iOS too, but the Python/Dart code here only configures the Android side. iOS support would need `DarwinNotificationDetails` in the Dart layer.
- Desktop does nothing. On desktop, the service instantiates without error but notifications won't appear since there's no native plugin backing it.

## How it works

Flet's architecture is `Python <-> Flet protocol <-> Flutter/Dart <-> platform APIs`. Python can't call Android APIs directly. This extension bridges that gap:

```
your Python app
  -> FletAndroidNotifications (ft.Service)
    -> _invoke_method() over Flet protocol
      -> NotificationsService (FletService, Dart)
        -> flutter_local_notifications plugin
          -> Android NotificationManager
```

The extension is packaged as a standard Python package with a `flutter/` namespace directory containing the Dart code. When you run `flet build apk`, Flet discovers the Dart code in site-packages and includes it as a path dependency in the generated Flutter project.

## License

MIT
