android-widgets 0.1.6__tar.gz → 0.1.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- android_widgets-0.1.8/PKG-INFO +239 -0
- android_widgets-0.1.8/README.md +217 -0
- {android_widgets-0.1.6 → android_widgets-0.1.8}/android_widgets/__init__.py +15 -1
- android_widgets-0.1.8/android_widgets/__main__.py +4 -0
- android_widgets-0.1.8/android_widgets/generate/__init__.py +32 -0
- android_widgets-0.1.8/android_widgets/generate/bindings.py +58 -0
- android_widgets-0.1.8/android_widgets/generate/cli.py +102 -0
- android_widgets-0.1.8/android_widgets/generate/drawables.py +25 -0
- android_widgets-0.1.8/android_widgets/generate/generator.py +103 -0
- android_widgets-0.1.8/android_widgets/generate/java.py +164 -0
- android_widgets-0.1.8/android_widgets/generate/layout.py +215 -0
- android_widgets-0.1.8/android_widgets/generate/manifest.py +161 -0
- android_widgets-0.1.8/android_widgets/generate/nodes.py +203 -0
- android_widgets-0.1.8/android_widgets/generate/provider.py +56 -0
- android_widgets-0.1.8/android_widgets/generate/values.py +19 -0
- android_widgets-0.1.8/android_widgets/generate/xml.py +72 -0
- {android_widgets-0.1.6 → android_widgets-0.1.8}/android_widgets/maker.py +4 -0
- android_widgets-0.1.8/android_widgets/runtime.py +139 -0
- android_widgets-0.1.8/android_widgets/templates/p4a_hook.py +33 -0
- android_widgets-0.1.8/android_widgets.egg-info/PKG-INFO +239 -0
- android_widgets-0.1.8/android_widgets.egg-info/SOURCES.txt +41 -0
- android_widgets-0.1.8/android_widgets.egg-info/entry_points.txt +2 -0
- android_widgets-0.1.8/android_widgets.egg-info/requires.txt +4 -0
- {android_widgets-0.1.6 → android_widgets-0.1.8}/android_widgets.egg-info/top_level.txt +1 -2
- {android_widgets-0.1.6 → android_widgets-0.1.8}/pyproject.toml +12 -3
- android_widgets-0.1.8/tests/sample.py +41 -0
- android_widgets-0.1.8/tests/test_cli.py +66 -0
- android_widgets-0.1.8/tests/test_generator.py +58 -0
- android_widgets-0.1.8/tests/test_generators.py +111 -0
- android_widgets-0.1.8/tests/test_java.py +125 -0
- android_widgets-0.1.8/tests/test_maker.py +51 -0
- android_widgets-0.1.8/tests/test_manifest.py +91 -0
- android_widgets-0.1.8/tests/test_naming.py +32 -0
- android_widgets-0.1.8/tests/test_nodes.py +45 -0
- android_widgets-0.1.8/tests/test_runtime.py +49 -0
- android_widgets-0.1.6/PKG-INFO +0 -154
- android_widgets-0.1.6/README.md +0 -136
- android_widgets-0.1.6/android_widgets.egg-info/PKG-INFO +0 -154
- android_widgets-0.1.6/android_widgets.egg-info/SOURCES.txt +0 -14
- android_widgets-0.1.6/venv/bin/activate_this.py +0 -36
- {android_widgets-0.1.6 → android_widgets-0.1.8}/android_widgets/aw_logging.py +0 -0
- {android_widgets-0.1.6 → android_widgets-0.1.8}/android_widgets/config.py +0 -0
- {android_widgets-0.1.6 → android_widgets-0.1.8}/android_widgets/facade.py +0 -0
- {android_widgets-0.1.6 → android_widgets-0.1.8}/android_widgets/java_classes.py +0 -0
- {android_widgets-0.1.6 → android_widgets-0.1.8}/android_widgets/tools.py +0 -0
- {android_widgets-0.1.6 → android_widgets-0.1.8}/android_widgets.egg-info/dependency_links.txt +0 -0
- {android_widgets-0.1.6 → android_widgets-0.1.8}/setup.cfg +0 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: android-widgets
|
|
3
|
+
Version: 0.1.8
|
|
4
|
+
Summary: This would be used to auto gen XMLs for Android Home Screen widgets.
|
|
5
|
+
Author-email: Fabian <fector101@yahoo.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Fector101/kivy-androidwidgets
|
|
8
|
+
Project-URL: Documentation, https://github.com/Fector101/kivy-androidwidgets
|
|
9
|
+
Project-URL: Source, https://github.com/Fector101/kivy-androidwidgets
|
|
10
|
+
Project-URL: Tracker, https://github.com/Fector101/kivy-androidwidgets/issues
|
|
11
|
+
Keywords: kivy,android,widgets,hello-world,python-package
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Operating System :: Android
|
|
14
|
+
Classifier: Development Status :: 3 - Alpha
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: kivy>=2.0.0
|
|
20
|
+
Provides-Extra: test
|
|
21
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
22
|
+
|
|
23
|
+
Kivy-androidwidgets
|
|
24
|
+
---
|
|
25
|
+
Generate Android home-screen widgets for Kivy / Buildozer apps from a pure-Python DSL — **no XML, no Java**. Widget definitions are compiled by a CLI pre-build step into `res/`, `src/` and a p4a hook that Buildozer compiles straight into the APK.
|
|
26
|
+
|
|
27
|
+
Complete sample: [working app](https://github.com/Fector101/wallpaper-carousel)
|
|
28
|
+
|
|
29
|
+
Quickstart
|
|
30
|
+
---
|
|
31
|
+
### 1. Install
|
|
32
|
+
```bash
|
|
33
|
+
pip install kivy-androidwidgets
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 2. Describe your widget in Python
|
|
37
|
+
|
|
38
|
+
`widgets.py` (in your project root):
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from android_widgets import HomeWidget, AWColumn, AWRow, AWText, AWImage, AWButton, AWProgressBar, Color
|
|
42
|
+
|
|
43
|
+
clock = HomeWidget(
|
|
44
|
+
name="Clock",
|
|
45
|
+
description="Shows the current time",
|
|
46
|
+
layout=AWColumn(
|
|
47
|
+
spacing=6,
|
|
48
|
+
children=[
|
|
49
|
+
AWText("12:00", id="time", size=28, bold=True),
|
|
50
|
+
AWText("Mon 4 Aug", id="date", size=12, color=Color("#888888")),
|
|
51
|
+
],
|
|
52
|
+
),
|
|
53
|
+
min_width_dp=110, # 2 cells
|
|
54
|
+
min_height_dp=110,
|
|
55
|
+
resize_mode="horizontal|vertical",
|
|
56
|
+
update_period_ms=1800000, # Android minimum: 30 minutes
|
|
57
|
+
tap_to_open=True, # whole widget opens the app on tap
|
|
58
|
+
background=Color("#FFFFFF"),
|
|
59
|
+
radius=12,
|
|
60
|
+
)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 3. Generate the Android artifacts
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
python -m android_widgets init # creates p4a/hook.py (one-time)
|
|
67
|
+
python -m android_widgets generate --spec buildozer.spec --module widgets.py
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
This writes (gitignore them):
|
|
71
|
+
- `res/layout/<name>.xml`, `res/layout/<name>_preview.xml`
|
|
72
|
+
- `res/xml/<name>_provider.xml` — the `appwidget-provider` metadata
|
|
73
|
+
- `res/drawable/widget_bg_<name>.xml` — rounded background shapes
|
|
74
|
+
- `src/<package>/<ClassName>.java` — one `AppWidgetProvider` per widget
|
|
75
|
+
- `res/values/widgets.xml`, `p4a/widget_receivers.json`
|
|
76
|
+
|
|
77
|
+
`generate` reads `package.domain` + `package.name` from `buildozer.spec`. Use `--dry-run` to preview without writing.
|
|
78
|
+
|
|
79
|
+
### 4. Wire it into `buildozer.spec`
|
|
80
|
+
|
|
81
|
+
```ini
|
|
82
|
+
android.add_resources = res
|
|
83
|
+
android.add_src = src
|
|
84
|
+
p4a.hook = p4a/hook.py
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 5. Build
|
|
88
|
+
```bash
|
|
89
|
+
buildozer android debug
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The generated `p4a/hook.py` (`after_apk_build`) injects the `<receiver>` entries into the dist's `AndroidManifest.xml` before aapt compiles it, so no manual manifest editing.
|
|
93
|
+
|
|
94
|
+
DSL Reference
|
|
95
|
+
---
|
|
96
|
+
- `HomeWidget` — `name`, `description`, `layout`, `min_width_dp`, `min_height_dp`, `target_cells`, `max_cells`, `resize_mode`, `widget_category`, `update_period_ms`, `preview_image`, `preview_layout`, `tap_to_open`, `background` (`Color`), `radius`, `padding`
|
|
97
|
+
- `AWColumn`, `AWRow` — `spacing`, `children`
|
|
98
|
+
- `AWFrame` — single child, ideal for cards
|
|
99
|
+
- `AWText` — `text`, `size`, `color`, `bold`, `italic`, `align`, `max_lines`, `id`
|
|
100
|
+
- `AWImage` — `resource`, `path`, `scale_type`, `content_description`, `id`
|
|
101
|
+
- `AWButton` — `text`, `id`
|
|
102
|
+
- `AWProgressBar` — `style`, `max`, `id`
|
|
103
|
+
|
|
104
|
+
Layout children with an `id` become runtime handles: the generated Java loads images from `context.getFilesDir()/app/assets/...` for `AWImage(path=...)` at add-time. `tap_to_open=True` wires `@+id/widget_root` to a `PendingIntent` opening the app (with the API-23 `FLAG_IMMUTABLE` guard).
|
|
105
|
+
|
|
106
|
+
Runtime updates
|
|
107
|
+
---
|
|
108
|
+
Add widgets to the home screen from Python at runtime (see `using-python-to-update-widget.md` for the manual version). `update_period_ms` cannot go below 1 800 000 ms on Android.
|
|
109
|
+
|
|
110
|
+
Widget & feature recipes: [examples.md](examples.md) (clock, battery meter, photo, button, card layouts, multiple widgets per app, and the runtime update API).
|
|
111
|
+
|
|
112
|
+
Tests
|
|
113
|
+
---
|
|
114
|
+
```bash
|
|
115
|
+
python3 -m unittest discover -s tests -v
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Manual fallback
|
|
119
|
+
---
|
|
120
|
+
Prefer the DSL above. If you need full control, the original manual method still works: hand-write the layout/provider XML, the Java `AppWidgetProvider` and the manifest hook (see below).
|
|
121
|
+
|
|
122
|
+
### Step 1: Design the layout
|
|
123
|
+
Store in `res/layout/simple_widget.xml`:
|
|
124
|
+
```xml
|
|
125
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
126
|
+
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
127
|
+
android:orientation="vertical"
|
|
128
|
+
android:padding="10dp"
|
|
129
|
+
android:background="#FFFFFF"
|
|
130
|
+
android:gravity="center"
|
|
131
|
+
android:layout_width="wrap_content"
|
|
132
|
+
android:layout_height="wrap_content">
|
|
133
|
+
|
|
134
|
+
<TextView
|
|
135
|
+
android:id="@+id/widget_text"
|
|
136
|
+
android:text="Loading..."
|
|
137
|
+
android:textSize="18sp"
|
|
138
|
+
android:textColor="#000"
|
|
139
|
+
android:layout_width="wrap_content"
|
|
140
|
+
android:layout_height="wrap_content"/>
|
|
141
|
+
|
|
142
|
+
</LinearLayout>
|
|
143
|
+
```
|
|
144
|
+
### Step 2: Create the widget info XML
|
|
145
|
+
Path: `res/xml/widgetproviderinfo.xml`
|
|
146
|
+
```xml
|
|
147
|
+
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
|
148
|
+
android:minWidth="120dp"
|
|
149
|
+
android:minHeight="60dp"
|
|
150
|
+
android:updatePeriodMillis="1800000"
|
|
151
|
+
android:initialLayout="@layout/simple_widget"
|
|
152
|
+
android:previewImage="@drawable/ic_launcher_foreground"
|
|
153
|
+
android:resizeMode="horizontal|vertical"
|
|
154
|
+
android:widgetCategory="home_screen">
|
|
155
|
+
</appwidget-provider>
|
|
156
|
+
```
|
|
157
|
+
Create the preview image at `res/drawable/ic_launcher_foreground.png`.
|
|
158
|
+
|
|
159
|
+
### Step 3: Create an `AppWidgetProvider`
|
|
160
|
+
Path: `src/SimpleWidget.java`
|
|
161
|
+
```java
|
|
162
|
+
package org.wally.waller; // Change here from buildozer.spec package.domain+package.name
|
|
163
|
+
|
|
164
|
+
import android.appwidget.AppWidgetManager;
|
|
165
|
+
import android.appwidget.AppWidgetProvider;
|
|
166
|
+
import android.content.Context;
|
|
167
|
+
import android.widget.RemoteViews;
|
|
168
|
+
|
|
169
|
+
import org.wally.waller.R; // Change here from buildozer.spec package.domain+package.name
|
|
170
|
+
import android.app.PendingIntent;
|
|
171
|
+
import android.content.Intent;
|
|
172
|
+
|
|
173
|
+
public class SimpleWidget extends AppWidgetProvider {
|
|
174
|
+
|
|
175
|
+
@Override
|
|
176
|
+
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
|
|
177
|
+
for (int appWidgetId : appWidgetIds) {
|
|
178
|
+
|
|
179
|
+
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.simple_widget);
|
|
180
|
+
|
|
181
|
+
// Example: Set text
|
|
182
|
+
views.setTextViewText(R.id.widget_text, "Hello Widget!");
|
|
183
|
+
|
|
184
|
+
// Update widget
|
|
185
|
+
appWidgetManager.updateAppWidget(appWidgetId, views);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Step 4: Inject the receiver with a p4a hook
|
|
192
|
+
Path: `p4a/hook.py`
|
|
193
|
+
```py
|
|
194
|
+
from pathlib import Path
|
|
195
|
+
from pythonforandroid.toolchain import ToolchainCL
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def after_apk_build(toolchain: ToolchainCL):
|
|
199
|
+
manifest_file = Path(toolchain._dist.dist_dir) / "src" / "main" / "AndroidManifest.xml"
|
|
200
|
+
text = manifest_file.read_text(encoding="utf-8")
|
|
201
|
+
|
|
202
|
+
package = "org.wally.waller"
|
|
203
|
+
receiver_xml = f'''
|
|
204
|
+
<receiver android:name="{package}.SimpleWidget"
|
|
205
|
+
android:enabled="true"
|
|
206
|
+
android:exported="false">
|
|
207
|
+
<intent-filter>
|
|
208
|
+
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
|
209
|
+
</intent-filter>
|
|
210
|
+
<meta-data android:name="android.appwidget.provider"
|
|
211
|
+
android:resource="@xml/widgetproviderinfo" />
|
|
212
|
+
</receiver>
|
|
213
|
+
'''
|
|
214
|
+
|
|
215
|
+
if receiver_xml.strip() not in text:
|
|
216
|
+
if "</application>" in text:
|
|
217
|
+
text = text.replace("</application>", f"{receiver_xml}\n</application>")
|
|
218
|
+
print("Receiver added")
|
|
219
|
+
else:
|
|
220
|
+
print("Could not find </application> to insert receiver")
|
|
221
|
+
else:
|
|
222
|
+
print("Receiver already exists in manifest")
|
|
223
|
+
|
|
224
|
+
manifest_file.write_text(text, encoding="utf-8")
|
|
225
|
+
print("Successfully_101: Manifest update completed successfully!")
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Step 5: Wire into `buildozer.spec`
|
|
229
|
+
```ini
|
|
230
|
+
android.add_resources = res
|
|
231
|
+
android.add_src = src
|
|
232
|
+
p4a.hook = p4a/hook.py
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Sample Image:
|
|
236
|
+
|
|
237
|
+

|
|
238
|
+
|
|
239
|
+
### For more widget customisation check: [How to Customise.md](how-to-customise.md)
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
Kivy-androidwidgets
|
|
2
|
+
---
|
|
3
|
+
Generate Android home-screen widgets for Kivy / Buildozer apps from a pure-Python DSL — **no XML, no Java**. Widget definitions are compiled by a CLI pre-build step into `res/`, `src/` and a p4a hook that Buildozer compiles straight into the APK.
|
|
4
|
+
|
|
5
|
+
Complete sample: [working app](https://github.com/Fector101/wallpaper-carousel)
|
|
6
|
+
|
|
7
|
+
Quickstart
|
|
8
|
+
---
|
|
9
|
+
### 1. Install
|
|
10
|
+
```bash
|
|
11
|
+
pip install kivy-androidwidgets
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
### 2. Describe your widget in Python
|
|
15
|
+
|
|
16
|
+
`widgets.py` (in your project root):
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from android_widgets import HomeWidget, AWColumn, AWRow, AWText, AWImage, AWButton, AWProgressBar, Color
|
|
20
|
+
|
|
21
|
+
clock = HomeWidget(
|
|
22
|
+
name="Clock",
|
|
23
|
+
description="Shows the current time",
|
|
24
|
+
layout=AWColumn(
|
|
25
|
+
spacing=6,
|
|
26
|
+
children=[
|
|
27
|
+
AWText("12:00", id="time", size=28, bold=True),
|
|
28
|
+
AWText("Mon 4 Aug", id="date", size=12, color=Color("#888888")),
|
|
29
|
+
],
|
|
30
|
+
),
|
|
31
|
+
min_width_dp=110, # 2 cells
|
|
32
|
+
min_height_dp=110,
|
|
33
|
+
resize_mode="horizontal|vertical",
|
|
34
|
+
update_period_ms=1800000, # Android minimum: 30 minutes
|
|
35
|
+
tap_to_open=True, # whole widget opens the app on tap
|
|
36
|
+
background=Color("#FFFFFF"),
|
|
37
|
+
radius=12,
|
|
38
|
+
)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 3. Generate the Android artifacts
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
python -m android_widgets init # creates p4a/hook.py (one-time)
|
|
45
|
+
python -m android_widgets generate --spec buildozer.spec --module widgets.py
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
This writes (gitignore them):
|
|
49
|
+
- `res/layout/<name>.xml`, `res/layout/<name>_preview.xml`
|
|
50
|
+
- `res/xml/<name>_provider.xml` — the `appwidget-provider` metadata
|
|
51
|
+
- `res/drawable/widget_bg_<name>.xml` — rounded background shapes
|
|
52
|
+
- `src/<package>/<ClassName>.java` — one `AppWidgetProvider` per widget
|
|
53
|
+
- `res/values/widgets.xml`, `p4a/widget_receivers.json`
|
|
54
|
+
|
|
55
|
+
`generate` reads `package.domain` + `package.name` from `buildozer.spec`. Use `--dry-run` to preview without writing.
|
|
56
|
+
|
|
57
|
+
### 4. Wire it into `buildozer.spec`
|
|
58
|
+
|
|
59
|
+
```ini
|
|
60
|
+
android.add_resources = res
|
|
61
|
+
android.add_src = src
|
|
62
|
+
p4a.hook = p4a/hook.py
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### 5. Build
|
|
66
|
+
```bash
|
|
67
|
+
buildozer android debug
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The generated `p4a/hook.py` (`after_apk_build`) injects the `<receiver>` entries into the dist's `AndroidManifest.xml` before aapt compiles it, so no manual manifest editing.
|
|
71
|
+
|
|
72
|
+
DSL Reference
|
|
73
|
+
---
|
|
74
|
+
- `HomeWidget` — `name`, `description`, `layout`, `min_width_dp`, `min_height_dp`, `target_cells`, `max_cells`, `resize_mode`, `widget_category`, `update_period_ms`, `preview_image`, `preview_layout`, `tap_to_open`, `background` (`Color`), `radius`, `padding`
|
|
75
|
+
- `AWColumn`, `AWRow` — `spacing`, `children`
|
|
76
|
+
- `AWFrame` — single child, ideal for cards
|
|
77
|
+
- `AWText` — `text`, `size`, `color`, `bold`, `italic`, `align`, `max_lines`, `id`
|
|
78
|
+
- `AWImage` — `resource`, `path`, `scale_type`, `content_description`, `id`
|
|
79
|
+
- `AWButton` — `text`, `id`
|
|
80
|
+
- `AWProgressBar` — `style`, `max`, `id`
|
|
81
|
+
|
|
82
|
+
Layout children with an `id` become runtime handles: the generated Java loads images from `context.getFilesDir()/app/assets/...` for `AWImage(path=...)` at add-time. `tap_to_open=True` wires `@+id/widget_root` to a `PendingIntent` opening the app (with the API-23 `FLAG_IMMUTABLE` guard).
|
|
83
|
+
|
|
84
|
+
Runtime updates
|
|
85
|
+
---
|
|
86
|
+
Add widgets to the home screen from Python at runtime (see `using-python-to-update-widget.md` for the manual version). `update_period_ms` cannot go below 1 800 000 ms on Android.
|
|
87
|
+
|
|
88
|
+
Widget & feature recipes: [examples.md](examples.md) (clock, battery meter, photo, button, card layouts, multiple widgets per app, and the runtime update API).
|
|
89
|
+
|
|
90
|
+
Tests
|
|
91
|
+
---
|
|
92
|
+
```bash
|
|
93
|
+
python3 -m unittest discover -s tests -v
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Manual fallback
|
|
97
|
+
---
|
|
98
|
+
Prefer the DSL above. If you need full control, the original manual method still works: hand-write the layout/provider XML, the Java `AppWidgetProvider` and the manifest hook (see below).
|
|
99
|
+
|
|
100
|
+
### Step 1: Design the layout
|
|
101
|
+
Store in `res/layout/simple_widget.xml`:
|
|
102
|
+
```xml
|
|
103
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
104
|
+
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
105
|
+
android:orientation="vertical"
|
|
106
|
+
android:padding="10dp"
|
|
107
|
+
android:background="#FFFFFF"
|
|
108
|
+
android:gravity="center"
|
|
109
|
+
android:layout_width="wrap_content"
|
|
110
|
+
android:layout_height="wrap_content">
|
|
111
|
+
|
|
112
|
+
<TextView
|
|
113
|
+
android:id="@+id/widget_text"
|
|
114
|
+
android:text="Loading..."
|
|
115
|
+
android:textSize="18sp"
|
|
116
|
+
android:textColor="#000"
|
|
117
|
+
android:layout_width="wrap_content"
|
|
118
|
+
android:layout_height="wrap_content"/>
|
|
119
|
+
|
|
120
|
+
</LinearLayout>
|
|
121
|
+
```
|
|
122
|
+
### Step 2: Create the widget info XML
|
|
123
|
+
Path: `res/xml/widgetproviderinfo.xml`
|
|
124
|
+
```xml
|
|
125
|
+
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
|
126
|
+
android:minWidth="120dp"
|
|
127
|
+
android:minHeight="60dp"
|
|
128
|
+
android:updatePeriodMillis="1800000"
|
|
129
|
+
android:initialLayout="@layout/simple_widget"
|
|
130
|
+
android:previewImage="@drawable/ic_launcher_foreground"
|
|
131
|
+
android:resizeMode="horizontal|vertical"
|
|
132
|
+
android:widgetCategory="home_screen">
|
|
133
|
+
</appwidget-provider>
|
|
134
|
+
```
|
|
135
|
+
Create the preview image at `res/drawable/ic_launcher_foreground.png`.
|
|
136
|
+
|
|
137
|
+
### Step 3: Create an `AppWidgetProvider`
|
|
138
|
+
Path: `src/SimpleWidget.java`
|
|
139
|
+
```java
|
|
140
|
+
package org.wally.waller; // Change here from buildozer.spec package.domain+package.name
|
|
141
|
+
|
|
142
|
+
import android.appwidget.AppWidgetManager;
|
|
143
|
+
import android.appwidget.AppWidgetProvider;
|
|
144
|
+
import android.content.Context;
|
|
145
|
+
import android.widget.RemoteViews;
|
|
146
|
+
|
|
147
|
+
import org.wally.waller.R; // Change here from buildozer.spec package.domain+package.name
|
|
148
|
+
import android.app.PendingIntent;
|
|
149
|
+
import android.content.Intent;
|
|
150
|
+
|
|
151
|
+
public class SimpleWidget extends AppWidgetProvider {
|
|
152
|
+
|
|
153
|
+
@Override
|
|
154
|
+
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
|
|
155
|
+
for (int appWidgetId : appWidgetIds) {
|
|
156
|
+
|
|
157
|
+
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.simple_widget);
|
|
158
|
+
|
|
159
|
+
// Example: Set text
|
|
160
|
+
views.setTextViewText(R.id.widget_text, "Hello Widget!");
|
|
161
|
+
|
|
162
|
+
// Update widget
|
|
163
|
+
appWidgetManager.updateAppWidget(appWidgetId, views);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Step 4: Inject the receiver with a p4a hook
|
|
170
|
+
Path: `p4a/hook.py`
|
|
171
|
+
```py
|
|
172
|
+
from pathlib import Path
|
|
173
|
+
from pythonforandroid.toolchain import ToolchainCL
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def after_apk_build(toolchain: ToolchainCL):
|
|
177
|
+
manifest_file = Path(toolchain._dist.dist_dir) / "src" / "main" / "AndroidManifest.xml"
|
|
178
|
+
text = manifest_file.read_text(encoding="utf-8")
|
|
179
|
+
|
|
180
|
+
package = "org.wally.waller"
|
|
181
|
+
receiver_xml = f'''
|
|
182
|
+
<receiver android:name="{package}.SimpleWidget"
|
|
183
|
+
android:enabled="true"
|
|
184
|
+
android:exported="false">
|
|
185
|
+
<intent-filter>
|
|
186
|
+
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
|
187
|
+
</intent-filter>
|
|
188
|
+
<meta-data android:name="android.appwidget.provider"
|
|
189
|
+
android:resource="@xml/widgetproviderinfo" />
|
|
190
|
+
</receiver>
|
|
191
|
+
'''
|
|
192
|
+
|
|
193
|
+
if receiver_xml.strip() not in text:
|
|
194
|
+
if "</application>" in text:
|
|
195
|
+
text = text.replace("</application>", f"{receiver_xml}\n</application>")
|
|
196
|
+
print("Receiver added")
|
|
197
|
+
else:
|
|
198
|
+
print("Could not find </application> to insert receiver")
|
|
199
|
+
else:
|
|
200
|
+
print("Receiver already exists in manifest")
|
|
201
|
+
|
|
202
|
+
manifest_file.write_text(text, encoding="utf-8")
|
|
203
|
+
print("Successfully_101: Manifest update completed successfully!")
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Step 5: Wire into `buildozer.spec`
|
|
207
|
+
```ini
|
|
208
|
+
android.add_resources = res
|
|
209
|
+
android.add_src = src
|
|
210
|
+
p4a.hook = p4a/hook.py
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Sample Image:
|
|
214
|
+
|
|
215
|
+

|
|
216
|
+
|
|
217
|
+
### For more widget customisation check: [How to Customise.md](how-to-customise.md)
|
|
@@ -12,7 +12,7 @@ from .config import get_python_activity_context
|
|
|
12
12
|
try:
|
|
13
13
|
from .java_classes import *
|
|
14
14
|
except Exception as e:
|
|
15
|
-
|
|
15
|
+
logger.debug(f"Import Java classes Error: {e}")
|
|
16
16
|
from .facade import *
|
|
17
17
|
|
|
18
18
|
RemoteViews_ = RemoteViews
|
|
@@ -20,6 +20,20 @@ except Exception as e:
|
|
|
20
20
|
|
|
21
21
|
from .tools import SpecFile
|
|
22
22
|
|
|
23
|
+
from .generate.nodes import (
|
|
24
|
+
Color,
|
|
25
|
+
HomeWidget,
|
|
26
|
+
AWButton,
|
|
27
|
+
AWColumn,
|
|
28
|
+
AWFrame,
|
|
29
|
+
AWImage,
|
|
30
|
+
AWProgressBar,
|
|
31
|
+
AWRow,
|
|
32
|
+
AWText,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
from .runtime import get_widget_data, save_widget_data, update_widget
|
|
36
|
+
|
|
23
37
|
if is_platform_android():
|
|
24
38
|
enable_logging()
|
|
25
39
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Build-time generators: turn the Python DSL into Android res/src artifacts."""
|
|
2
|
+
|
|
3
|
+
from .nodes import (
|
|
4
|
+
Color,
|
|
5
|
+
HomeWidget,
|
|
6
|
+
AWButton,
|
|
7
|
+
AWColumn,
|
|
8
|
+
AWFrame,
|
|
9
|
+
AWImage,
|
|
10
|
+
AWProgressBar,
|
|
11
|
+
AWRow,
|
|
12
|
+
AWText,
|
|
13
|
+
)
|
|
14
|
+
from .generator import collect_widgets, generate
|
|
15
|
+
from .manifest import inject_receivers, inject_widget_receivers, receivers_from_widgets
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"Color",
|
|
19
|
+
"HomeWidget",
|
|
20
|
+
"AWButton",
|
|
21
|
+
"AWColumn",
|
|
22
|
+
"AWFrame",
|
|
23
|
+
"AWImage",
|
|
24
|
+
"AWProgressBar",
|
|
25
|
+
"AWRow",
|
|
26
|
+
"AWText",
|
|
27
|
+
"collect_widgets",
|
|
28
|
+
"generate",
|
|
29
|
+
"inject_receivers",
|
|
30
|
+
"inject_widget_receivers",
|
|
31
|
+
"receivers_from_widgets",
|
|
32
|
+
]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Describe which layout nodes are runtime-updatable and their data keys.
|
|
2
|
+
|
|
3
|
+
Both the generated Java (reads the keys in onUpdate) and the Python runtime
|
|
4
|
+
(writes the keys before broadcasting an update) must agree on the key format,
|
|
5
|
+
so this module is shared and kept free of Android dependencies.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import List, Optional
|
|
10
|
+
|
|
11
|
+
from . import nodes
|
|
12
|
+
from .xml import to_snake
|
|
13
|
+
|
|
14
|
+
PREFERENCES_NAME = "android_widgets"
|
|
15
|
+
|
|
16
|
+
TEXT = "text"
|
|
17
|
+
BUTTON = "button"
|
|
18
|
+
IMAGE = "image"
|
|
19
|
+
PROGRESS = "progress"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class DataBinding:
|
|
24
|
+
view_id: str
|
|
25
|
+
kind: str
|
|
26
|
+
default: str
|
|
27
|
+
max: Optional[int] = None
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def java_setter(self) -> str:
|
|
31
|
+
return {
|
|
32
|
+
TEXT: "setTextViewText",
|
|
33
|
+
BUTTON: "setTextViewText",
|
|
34
|
+
IMAGE: "setImageViewBitmap",
|
|
35
|
+
PROGRESS: "setProgressBar",
|
|
36
|
+
}[self.kind]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def data_bindings(home_widget: nodes.HomeWidget) -> List[DataBinding]:
|
|
40
|
+
"""Collect runtime-updatable nodes (those with an id) from the tree."""
|
|
41
|
+
binds: List[DataBinding] = []
|
|
42
|
+
for node in home_widget.layout.iter_nodes():
|
|
43
|
+
if not node.id:
|
|
44
|
+
continue
|
|
45
|
+
if isinstance(node, nodes.AWText):
|
|
46
|
+
binds.append(DataBinding(node.id, TEXT, node.text))
|
|
47
|
+
elif isinstance(node, nodes.AWButton):
|
|
48
|
+
binds.append(DataBinding(node.id, BUTTON, node.text))
|
|
49
|
+
elif isinstance(node, nodes.AWProgressBar):
|
|
50
|
+
binds.append(DataBinding(node.id, PROGRESS, "0", node.max))
|
|
51
|
+
elif isinstance(node, nodes.AWImage) and node.path:
|
|
52
|
+
binds.append(DataBinding(node.id, IMAGE, node.path))
|
|
53
|
+
return binds
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def data_key(widget_name: str, view_id: str) -> str:
|
|
57
|
+
"""SharedPreferences key for a runtime-updatable field."""
|
|
58
|
+
return f"{to_snake(widget_name)}.{view_id}"
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Command line interface: `python -m android_widgets generate`."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from ..tools import SpecFile
|
|
8
|
+
from .generator import collect_widgets, generate, load_widget_module
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _package_from_spec(spec_path: str) -> str:
|
|
12
|
+
spec = SpecFile(spec_path)
|
|
13
|
+
domain = spec.get("app", "package.domain")
|
|
14
|
+
name = spec.get("app", "package.name")
|
|
15
|
+
if not domain or not name:
|
|
16
|
+
raise SystemExit(
|
|
17
|
+
f"Error: buildozer.spec at {spec_path} is missing "
|
|
18
|
+
f"package.domain or package.name"
|
|
19
|
+
)
|
|
20
|
+
return f"{domain}.{name}"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def cmd_generate(args) -> int:
|
|
24
|
+
if not os.path.exists(args.spec):
|
|
25
|
+
raise SystemExit(f"Error: buildozer.spec not found at {args.spec}")
|
|
26
|
+
package = _package_from_spec(args.spec)
|
|
27
|
+
|
|
28
|
+
module_path = os.path.abspath(args.module)
|
|
29
|
+
if not os.path.exists(module_path):
|
|
30
|
+
raise SystemExit(f"Error: widget module not found at {module_path}")
|
|
31
|
+
module = load_widget_module(module_path, base_dir=args.out)
|
|
32
|
+
widgets = collect_widgets(module)
|
|
33
|
+
if not widgets:
|
|
34
|
+
print(f"Warning: no HomeWidget definitions found in {module_path}")
|
|
35
|
+
|
|
36
|
+
written = generate(widgets, package=package, out_dir=args.out, dry_run=args.dry_run)
|
|
37
|
+
|
|
38
|
+
mode = "Would generate" if args.dry_run else "Generated"
|
|
39
|
+
print(f"{mode} {len(written)} file(s) for {len(widgets)} widget(s):")
|
|
40
|
+
for path in written:
|
|
41
|
+
print(f" {path}")
|
|
42
|
+
if not args.dry_run and widgets:
|
|
43
|
+
print("\nNext: add these to buildozer.spec and run 'buildozer android debug':")
|
|
44
|
+
print(" android.add_resources = res")
|
|
45
|
+
print(" android.add_src = src")
|
|
46
|
+
print(" p4a.hook = p4a/hook.py")
|
|
47
|
+
return 0
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def cmd_init(args) -> int:
|
|
51
|
+
import shutil
|
|
52
|
+
|
|
53
|
+
template = os.path.join(
|
|
54
|
+
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
|
55
|
+
"templates",
|
|
56
|
+
"p4a_hook.py",
|
|
57
|
+
)
|
|
58
|
+
out_dir = os.path.join(args.out, "p4a")
|
|
59
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
60
|
+
dest = os.path.join(out_dir, "hook.py")
|
|
61
|
+
if os.path.exists(dest) and not args.force:
|
|
62
|
+
print(f"Info: {dest} already exists (use --force to overwrite)")
|
|
63
|
+
return 0
|
|
64
|
+
shutil.copy(template, dest)
|
|
65
|
+
print(f"Created {dest}")
|
|
66
|
+
return 0
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
70
|
+
parser = argparse.ArgumentParser(
|
|
71
|
+
prog="android-widgets",
|
|
72
|
+
description="Generate Android home screen widget resources for Kivy apps.",
|
|
73
|
+
)
|
|
74
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
75
|
+
|
|
76
|
+
gen = sub.add_parser("generate", help="Generate res/ + src/ + p4a hook data")
|
|
77
|
+
gen.add_argument("--spec", default="buildozer.spec", help="path to buildozer.spec")
|
|
78
|
+
gen.add_argument(
|
|
79
|
+
"--module",
|
|
80
|
+
default="widgets.py",
|
|
81
|
+
help="Python module containing the HomeWidget definitions",
|
|
82
|
+
)
|
|
83
|
+
gen.add_argument("--out", default=".", help="project directory to write into")
|
|
84
|
+
gen.add_argument("--dry-run", action="store_true", help="print files without writing")
|
|
85
|
+
gen.set_defaults(func=cmd_generate)
|
|
86
|
+
|
|
87
|
+
init = sub.add_parser("init", help="Scaffold p4a/hook.py into the project")
|
|
88
|
+
init.add_argument("--out", default=".", help="project directory")
|
|
89
|
+
init.add_argument("--force", action="store_true", help="overwrite existing hook.py")
|
|
90
|
+
init.set_defaults(func=cmd_init)
|
|
91
|
+
|
|
92
|
+
return parser
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def main(argv=None) -> int:
|
|
96
|
+
parser = build_parser()
|
|
97
|
+
args = parser.parse_args(argv)
|
|
98
|
+
return args.func(args)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
if __name__ == "__main__":
|
|
102
|
+
sys.exit(main())
|