android-widgets 0.1.0__tar.gz → 0.1.2__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.
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: android-widgets
3
- Version: 0.1.0
4
- Summary: A simple hello world package for Kivy Android widgets.
3
+ Version: 0.1.2
4
+ Summary: This would be used to auto gen XMLs for Android Home Screen widgets.
5
5
  Author-email: Fabian <fector101@yahoo.com>
6
6
  License: MIT
7
- Project-URL: Homepage, https://github.com/yourusername/kivy-androidwidgets
8
- Project-URL: Documentation, https://github.com/yourusername/kivy-androidwidgets
9
- Project-URL: Source, https://github.com/yourusername/kivy-androidwidgets
10
- Project-URL: Tracker, https://github.com/yourusername/kivy-androidwidgets/issues
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
11
  Keywords: kivy,android,widgets,hello-world,python-package
12
12
  Classifier: Programming Language :: Python :: 3
13
13
  Classifier: License :: OSI Approved :: MIT License
@@ -27,7 +27,7 @@ Complete Sample: [working app](https://github.com/Fector101/wallpaper-carousel)
27
27
  5 Steps For a simple widget
28
28
  ---
29
29
 
30
- step 1: First you Design How you want the Widget to Look [it's Layout].
30
+ ### step 1: First you Design How you want the Widget to Look [it's Layout].
31
31
  Store it in: `res/layout/simple_widget.xml`
32
32
  This a simple widget with a text
33
33
  ```xml
@@ -50,7 +50,7 @@ This a simple widget with a text
50
50
 
51
51
  </LinearLayout>
52
52
  ```
53
- step 2: Create an xml containing the info about the widget.
53
+ ### step 2: Create an xml containing the info about the widget.
54
54
  Like: size, preview icon and others
55
55
  path: `res/xml/widgetproviderinfo.xml`
56
56
  ```xml
@@ -66,7 +66,7 @@ path: `res/xml/widgetproviderinfo.xml`
66
66
  ```
67
67
  Create preview image png in right path`res/drawable/ic_launcher_foreground.png`
68
68
 
69
- step 3: Create a `AppWidgetProvider` it's used to receive events for widget.
69
+ ### step 3: Create a `AppWidgetProvider` it's used to receive events for widget.
70
70
  path: `src/SimpleWidget.java`.
71
71
  This will receive an event when widget is add to change it's text
72
72
 
@@ -99,7 +99,8 @@ public class SimpleWidget extends AppWidgetProvider {
99
99
  }
100
100
  }
101
101
  ```
102
- Step 4: Automate injecting Receiver in XML
102
+
103
+ ### Step 4: Automate injecting Receiver in XML
103
104
 
104
105
  path:`p4a/hook.py`
105
106
 
@@ -138,16 +139,19 @@ def after_apk_build(toolchain: ToolchainCL):
138
139
  print("Successfully_101: Manifest update completed successfully!")
139
140
 
140
141
  ```
141
- Step 5: From `buildozer.spec` tell it you want to add resources, src and p4a hook
142
+
143
+ ### Step 5: From `buildozer.spec` tell it you want to add resources, src and p4a hook
142
144
  ```ini
143
145
  android.add_resources = res
144
146
  android.add_src = src
145
147
  p4a.hook = p4a/hook.py
146
148
  ```
147
149
 
148
- For More widget customisation check: [How to Customise.md](how-to-customise.md)
149
- ---
150
150
 
151
151
  Sample Image:
152
152
 
153
153
  ![Rounded corners widget](https://raw.githubusercontent.com/Fector101/kivy-androidwidgets/main/imgs/not-rounded.jpg)
154
+
155
+
156
+ ### For More widget customisation check: [How to Customise.md](how-to-customise.md)
157
+
@@ -6,7 +6,7 @@ Complete Sample: [working app](https://github.com/Fector101/wallpaper-carousel)
6
6
  5 Steps For a simple widget
7
7
  ---
8
8
 
9
- step 1: First you Design How you want the Widget to Look [it's Layout].
9
+ ### step 1: First you Design How you want the Widget to Look [it's Layout].
10
10
  Store it in: `res/layout/simple_widget.xml`
11
11
  This a simple widget with a text
12
12
  ```xml
@@ -29,7 +29,7 @@ This a simple widget with a text
29
29
 
30
30
  </LinearLayout>
31
31
  ```
32
- step 2: Create an xml containing the info about the widget.
32
+ ### step 2: Create an xml containing the info about the widget.
33
33
  Like: size, preview icon and others
34
34
  path: `res/xml/widgetproviderinfo.xml`
35
35
  ```xml
@@ -45,7 +45,7 @@ path: `res/xml/widgetproviderinfo.xml`
45
45
  ```
46
46
  Create preview image png in right path`res/drawable/ic_launcher_foreground.png`
47
47
 
48
- step 3: Create a `AppWidgetProvider` it's used to receive events for widget.
48
+ ### step 3: Create a `AppWidgetProvider` it's used to receive events for widget.
49
49
  path: `src/SimpleWidget.java`.
50
50
  This will receive an event when widget is add to change it's text
51
51
 
@@ -78,7 +78,8 @@ public class SimpleWidget extends AppWidgetProvider {
78
78
  }
79
79
  }
80
80
  ```
81
- Step 4: Automate injecting Receiver in XML
81
+
82
+ ### Step 4: Automate injecting Receiver in XML
82
83
 
83
84
  path:`p4a/hook.py`
84
85
 
@@ -117,16 +118,19 @@ def after_apk_build(toolchain: ToolchainCL):
117
118
  print("Successfully_101: Manifest update completed successfully!")
118
119
 
119
120
  ```
120
- Step 5: From `buildozer.spec` tell it you want to add resources, src and p4a hook
121
+
122
+ ### Step 5: From `buildozer.spec` tell it you want to add resources, src and p4a hook
121
123
  ```ini
122
124
  android.add_resources = res
123
125
  android.add_src = src
124
126
  p4a.hook = p4a/hook.py
125
127
  ```
126
128
 
127
- For More widget customisation check: [How to Customise.md](how-to-customise.md)
128
- ---
129
129
 
130
130
  Sample Image:
131
131
 
132
132
  ![Rounded corners widget](https://raw.githubusercontent.com/Fector101/kivy-androidwidgets/main/imgs/not-rounded.jpg)
133
+
134
+
135
+ ### For More widget customisation check: [How to Customise.md](how-to-customise.md)
136
+
@@ -0,0 +1,206 @@
1
+
2
+ import os
3
+ import traceback
4
+
5
+ from .config import is_platform_android
6
+ from .aw_logging import logger, enable_logging
7
+
8
+
9
+ __all__ = ["logger"]
10
+
11
+ from .config import get_python_activity_context
12
+
13
+ try:
14
+ from .java_classes import *
15
+ except Exception as e:
16
+ print("[android_widgets] Import Java classes Error:", e)
17
+ from .facade import *
18
+
19
+ RemoteViews_ = RemoteViews
20
+ AppWidgetManager_ = AppWidgetManager
21
+
22
+ from .tools import SpecFile
23
+
24
+ if is_platform_android():
25
+ enable_logging()
26
+
27
+ def get_resources():
28
+ context = get_python_activity_context()
29
+ return context.getResources()
30
+
31
+ def get_package_name():
32
+ context = get_python_activity_context()
33
+ return context.getPackageName() # package.domain + "." + package.name
34
+
35
+ def get_resource(name, folder_base):
36
+ resources = get_resources()
37
+ package_name = get_package_name()
38
+ print("resources.getIdentifier",name, folder_base,"id:",resources.getIdentifier(name, folder_base, package_name))
39
+ return resources.getIdentifier(name, folder_base, package_name)
40
+
41
+ def get_buildozer_spec_file_path():
42
+ return os.path.join(os.getcwd(), "buildozer.spec")
43
+
44
+
45
+ class Layout:
46
+ def __init__(self, layout_name):
47
+ """
48
+
49
+ :param layout_name: base name without '.xml'
50
+ """
51
+ self.layout_name = layout_name
52
+ self.id = get_resource(layout_name, "layout")
53
+ self.check_if_layout_exists()
54
+
55
+ @property
56
+ def layout_path(self):
57
+ specFile = SpecFile(get_buildozer_spec_file_path())
58
+ resources_folder = specFile.get("app", "android.add_resources", '')
59
+ layout_path = os.path.join(resources_folder, 'layout', self.layout_name + '.xml')
60
+ return layout_path
61
+
62
+ def check_if_layout_exists(self):
63
+ if is_platform_android():
64
+ if not self.id:
65
+ logger.error(f"Layout doesn't exist: {self.layout_name}")
66
+ return None
67
+
68
+ spec_file_path = get_buildozer_spec_file_path()
69
+ # This not a real path on android /data/data/org.wally.waller/files/app/buildozer.spec
70
+
71
+ if not os.path.exists(spec_file_path):
72
+ logger.debug(f"buildozer.spec file not found at: {spec_file_path}")
73
+ return None
74
+
75
+ if not os.path.exists(self.layout_path):
76
+ logger.error(f"Layout doesn't exist: {self.layout_path}")
77
+ return None
78
+ else:
79
+ logger.info(f"Found Layout: {self.layout_path}")
80
+ return None
81
+
82
+
83
+ class RemoteViews:
84
+ def __init__(self, layout:Layout):
85
+ self.layout = layout
86
+ layout_id = layout.id
87
+ package_name = get_package_name()
88
+ self.main = RemoteViews_(package_name, layout_id)
89
+
90
+ def setTextViewText(self, text_id, text):
91
+ resources = get_resources()
92
+ package_name = get_package_name()
93
+ __text_id = resources.getIdentifier(text_id, "id",package_name)
94
+ # print("text_id:", __text_id)
95
+ if not __text_id:
96
+ logger.error(f"Text ID doesn't exist: No @+id/{text_id} in res/layout/{self.layout.layout_name}.xml")
97
+ return None
98
+ self.main.setTextViewText(__text_id, String(text))
99
+ return None
100
+
101
+
102
+ class AppWidgetManager:
103
+ def __init__(self, java_class_name):
104
+ self.java_class_name = java_class_name
105
+ context = get_python_activity_context()
106
+ package_name = get_package_name()
107
+ component = ComponentName(context, f'{package_name}.{java_class_name}')
108
+ self.main = AppWidgetManager_.getInstance(context)
109
+ self.widget_ids = self.main.getAppWidgetIds(component)
110
+ self.check_if_java_file_exists()
111
+
112
+ def updateAppWidget(self, java_view_object):
113
+ self.main.updateAppWidget(self.widget_ids, java_view_object)
114
+
115
+ @property
116
+ def java_file_path(self):
117
+ specFile = SpecFile(get_buildozer_spec_file_path())
118
+ jave_src_folder = specFile.get("app", "android.add_src", '')
119
+ java_file_path__ = os.path.join(jave_src_folder, self.java_class_name + '.java')
120
+ return java_file_path__
121
+
122
+ def check_if_java_file_exists(self):
123
+ spec_file_path = get_buildozer_spec_file_path()
124
+ if is_platform_android():
125
+ if not self.widget_ids: # when wrong .java file name is given self.widget_ids == []
126
+ logger.warning(f"Java File might not exist: {self.java_class_name}.java")
127
+ return None
128
+
129
+ if not os.path.exists(spec_file_path):
130
+ logger.debug(f"buildozer.spec file not found at: {spec_file_path}")
131
+ return None
132
+
133
+ java_file_path = self.java_file_path
134
+ if not os.path.exists(java_file_path):
135
+ logger.error(f"Java File doesn't exist: {java_file_path}")
136
+ # TODO Write emtpy java file
137
+ return None
138
+ else:
139
+ logger.info(f"Found Layout: {java_file_path}")
140
+ return None
141
+
142
+ # try:
143
+ # print("-"*5,'test 1','-'*5)
144
+ # appWidgetManager = AppWidgetManager("Image")
145
+ # print("The widget_ids:", appWidgetManager.widget_ids)
146
+ #
147
+ # text_layout = Layout("image_test_widget")
148
+ # views = RemoteViews(layout=text_layout)
149
+ # views.setTextViewText(text_id="widget_text", text="Frm py 1")
150
+ #
151
+ # appWidgetManager.updateAppWidget(java_view_object=views.main)
152
+ # except Exception as e:
153
+ # print("-"*5,"test 1 Error:",e,"-"*5)
154
+ # traceback.print_exc()
155
+ #
156
+ # try:
157
+ # print("-"*5,'test 2','-'*5)
158
+ #
159
+ # appWidgetManager = AppWidgetManager("Image1")
160
+ # print("The widget_ids:", appWidgetManager.widget_ids)
161
+ #
162
+ # text_layout = Layout("image_test_widget1")
163
+ # views = RemoteViews(layout=text_layout)
164
+ # views.setTextViewText(text_id="widget_text", text="Frm py 2")
165
+ #
166
+ # appWidgetManager.updateAppWidget(java_view_object=views.main)
167
+ # except Exception as e:
168
+ # print("-" * 5, "test 2 Error:",e, "-" * 5)
169
+ # traceback.print_exc()
170
+ #
171
+ # try:
172
+ # print("-" * 5, 'test 3', '-' * 5)
173
+ #
174
+ # appWidgetManager = AppWidgetManager("Image1")
175
+ # print("The widget_ids:", appWidgetManager.widget_ids)
176
+ #
177
+ # text_layout = Layout("image_test_widget")
178
+ # views = RemoteViews(layout=text_layout)
179
+ # views.setTextViewText(text_id="widget_text3", text="Frm py 3")
180
+ #
181
+ # appWidgetManager.updateAppWidget(java_view_object=views.main)
182
+ # except Exception as e:
183
+ # print("-" * 5, "test 3 Error:",e, "-" * 5)
184
+ # traceback.print_exc()
185
+
186
+ # For Reference
187
+ # AppWidgetManager = autoclass('android.appwidget.AppWidgetManager')
188
+ # ComponentName = autoclass('android.content.ComponentName')
189
+ # RemoteViews = autoclass('android.widget.RemoteViews')
190
+ #
191
+ # context = get_python_activity_context() # PythonActivity.mActivity.getApplicationContext()
192
+ # resources = context.getResources()
193
+ # package_name = context.getPackageName()
194
+ #
195
+ # # IMPORTANT: use CLASS NAME STRING, NOT autoclass
196
+ # component = ComponentName( context, 'org.wally.waller.Image1' )
197
+ #
198
+ # appWidgetManager = AppWidgetManager.getInstance(context)
199
+ # ids = appWidgetManager.getAppWidgetIds(component)
200
+ #
201
+ # text_layout = resources.getIdentifier("image_test_widget", "layout", package_name)
202
+ # title_id = resources.getIdentifier("widget_text", "id", package_name)
203
+ #
204
+ # views = RemoteViews(package_name, text_layout)
205
+ # views.setTextViewText(title_id, AndroidString("Madness"))
206
+ # appWidgetManager.updateAppWidget(ids, views)
@@ -0,0 +1,58 @@
1
+ import logging
2
+
3
+
4
+ logger = logging.getLogger("android_widgets")
5
+ logger.addHandler(logging.NullHandler())
6
+ # logger.propagate = False #The point of package is to help developing with python on PC, so logs very Useful
7
+
8
+
9
+ # ANSI color codes
10
+ COLORS = {
11
+ "DEBUG": "\033[36m", # Cyan
12
+ "INFO": "\033[32m", # Green
13
+ "WARNING": "\033[33m", # Yellow
14
+ "ERROR": "\033[31m", # Red
15
+ "CRITICAL": "\033[41m", # Red background
16
+ }
17
+ RESET = "\033[0m"
18
+
19
+
20
+ class BracketColorFormatter(logging.Formatter):
21
+ def format(self, record):
22
+ # Fixed-width levelname
23
+ levelname = f"{record.levelname:<7}"
24
+ color = COLORS.get(record.levelname, "")
25
+ level_colored = f"{color}{levelname}{RESET}"
26
+
27
+ # Fixed-width logger name
28
+ name_fixed = f"{record.name:<15}"
29
+
30
+ # Build the final string manually to avoid any extra spaces
31
+ return f"[{level_colored}][{name_fixed}] {record.getMessage()}"
32
+
33
+
34
+ def enable_logging(level=logging.DEBUG):
35
+ """Enable colored logging for android_widgets logger."""
36
+ # Avoid adding multiple StreamHandlers
37
+ if any(isinstance(h, logging.StreamHandler) for h in logger.handlers):
38
+ return
39
+
40
+ handler = logging.StreamHandler()
41
+ handler.setFormatter(BracketColorFormatter())
42
+ logger.addHandler(handler)
43
+ logger.setLevel(level)
44
+
45
+ def disable_logging():
46
+ """Disable colored logging for android_widgets logger."""
47
+ # Remove all StreamHandlers (the ones we added in enable_colored_logging)
48
+ for handler in logger.handlers[:]:
49
+ if isinstance(handler, logging.StreamHandler):
50
+ logger.removeHandler(handler)
51
+ # Ensure logger stays silent
52
+ logger.addHandler(logging.NullHandler())
53
+ logger.setLevel(logging.NOTSET)
54
+
55
+ # ------------------ Test ------------------
56
+ if __name__ == "__main__":
57
+ enable_logging()
58
+ logger.info("Found Layout: app_src/android/res/layout/image_test_widget.xml")
@@ -0,0 +1,74 @@
1
+ import os
2
+ from jnius import autoclass
3
+
4
+
5
+ ON_ANDROID = False
6
+ def is_platform_android():
7
+ # Took this from kivy to fix my logs in P4A.hook, so no need to import things i don't need by doing `from kivy.utils import platform`
8
+ if os.getenv("MAIN_ACTIVITY_HOST_CLASS_NAME"):
9
+ return True
10
+ kivy_build = os.environ.get('KIVY_BUILD', '')
11
+ if kivy_build in {'android'}:
12
+ return True
13
+ elif 'P4A_BOOTSTRAP' in os.environ:
14
+ return True
15
+ elif 'ANDROID_ARGUMENT' in os.environ:
16
+ return True
17
+
18
+ return False
19
+
20
+
21
+ if is_platform_android():
22
+ ON_ANDROID = True
23
+
24
+ def from_service_file():
25
+ return 'PYTHON_SERVICE_ARGUMENT' in os.environ
26
+
27
+
28
+ def on_flet_app():
29
+ return os.getenv("MAIN_ACTIVITY_HOST_CLASS_NAME")
30
+
31
+ def get_activity_class_name():
32
+ ACTIVITY_CLASS_NAME = os.getenv("MAIN_ACTIVITY_HOST_CLASS_NAME") # flet python
33
+ if not ACTIVITY_CLASS_NAME:
34
+ try:
35
+ from android import config
36
+ ACTIVITY_CLASS_NAME = config.JAVA_NAMESPACE
37
+ except (ImportError, AttributeError):
38
+ ACTIVITY_CLASS_NAME = 'org.kivy.android'
39
+ return ACTIVITY_CLASS_NAME
40
+
41
+
42
+
43
+ def get_python_activity():
44
+ if not ON_ANDROID:
45
+ from .facade import PythonActivity
46
+ return PythonActivity
47
+ ACTIVITY_CLASS_NAME = get_activity_class_name()
48
+ if on_flet_app():
49
+ PythonActivity = autoclass(ACTIVITY_CLASS_NAME)
50
+ else:
51
+ PythonActivity = autoclass(ACTIVITY_CLASS_NAME + '.PythonActivity')
52
+ return PythonActivity
53
+
54
+
55
+ def get_python_service():
56
+ if not ON_ANDROID:
57
+ return None
58
+ PythonService = autoclass(get_activity_class_name() + '.PythonService')
59
+ return PythonService.mService
60
+
61
+
62
+ def get_python_activity_context():
63
+ if not ON_ANDROID:
64
+ from .facade import Context
65
+ return Context
66
+
67
+ PythonActivity = get_python_activity()
68
+ if from_service_file():
69
+ service = get_python_service()
70
+ context = service.getApplication().getApplicationContext()
71
+ else:
72
+ context = PythonActivity.mActivity
73
+ return context
74
+
@@ -0,0 +1,376 @@
1
+ """For autocomplete Storing Reference to Available Methods"""
2
+ from typing import Literal
3
+ from .aw_logging import logger
4
+
5
+ Importance = Literal['urgent', 'high', 'medium', 'low', 'none']
6
+ """
7
+ :argument urgent - Makes a sound and appears as a heads-up notification.
8
+ :argument high - Makes a sound.
9
+ :argument urgent - Makes no sound.
10
+ :argument urgent - Makes no sound and doesn't appear in the status bar.
11
+ :argument urgent - Makes no sound and doesn't in the status bar or shade.
12
+ """
13
+
14
+
15
+ # For Dev
16
+ # Idea for typing autocompletion and reference
17
+ class Bundle:
18
+ def putString(self, key, value):
19
+ logger.debug(f"[MOCK] Bundle.putString called with key={key}, value={value}")
20
+
21
+ def putInt(self, key, value):
22
+ logger.debug(f"[MOCK] Bundle.putInt called with key={key}, value={value}")
23
+
24
+
25
+ class String(str):
26
+ def __new__(cls, value):
27
+ logger.debug(f"[MOCK] String created with value={value}")
28
+ return str.__new__(cls, value)
29
+
30
+
31
+ class Intent:
32
+ FLAG_ACTIVITY_NEW_TASK = 'FACADE_FLAG_ACTIVITY_NEW_TASK'
33
+ CATEGORY_DEFAULT = 'FACADE_FLAG_CATEGORY_DEFAULT'
34
+
35
+ def __init__(self, context='', activity=''):
36
+ self.obj = {}
37
+ logger.debug(f"[MOCK] Intent initialized with context={context}, activity={activity}")
38
+
39
+ def setAction(self, action):
40
+ logger.debug(f"[MOCK] Intent.setAction called with: {action}")
41
+ return self
42
+
43
+ def addFlags(self, *flags):
44
+ logger.debug(f"[MOCK] Intent.addFlags called with: {flags}")
45
+ return self
46
+
47
+ def setData(self, uri):
48
+ logger.debug(f"[MOCK] Intent.setData called with: {uri}")
49
+ return self
50
+
51
+ def setFlags(self, intent_flag):
52
+ logger.debug(f"[MOCK] Intent.setFlags called with: {intent_flag}")
53
+ return self
54
+
55
+ def addCategory(self, intent_category):
56
+ logger.debug(f"[MOCK] Intent.addCategory called with: {intent_category}")
57
+ return self
58
+
59
+ def getAction(self):
60
+ logger.debug("[MOCK] Intent.getAction called")
61
+ return self
62
+
63
+ def getStringExtra(self, key):
64
+ logger.debug(f"[MOCK] Intent.getStringExtra called with key={key}")
65
+ return self
66
+
67
+ def putExtra(self, key, value):
68
+ self.obj[key] = value
69
+ logger.debug(f"[MOCK] Intent.putExtra called with key={key}, value={value}")
70
+
71
+ def putExtras(self, bundle: Bundle):
72
+ self.obj['bundle'] = bundle
73
+ logger.debug(f"[MOCK] Intent.putExtras called with bundle={bundle}")
74
+
75
+
76
+ class PendingIntent:
77
+ FLAG_IMMUTABLE = ''
78
+ FLAG_UPDATE_CURRENT = ''
79
+
80
+ def getActivity(self, context, value, action_intent, pending_intent_type):
81
+ logger.debug(
82
+ f"[MOCK] PendingIntent.getActivity called with "
83
+ f"context={context}, value={value}, action_intent={action_intent}, type={pending_intent_type}"
84
+ )
85
+
86
+
87
+ class BitmapFactory:
88
+ def decodeStream(self, stream):
89
+ logger.debug(f"[MOCK] BitmapFactory.decodeStream called with stream={stream}")
90
+
91
+
92
+ class BuildVersion:
93
+ SDK_INT = 0
94
+
95
+
96
+ class Manifest:
97
+ POST_NOTIFICATIONS = 'FACADE_IMPORT'
98
+
99
+
100
+ class Settings:
101
+ ACTION_APP_NOTIFICATION_SETTINGS = 'FACADE_IMPORT_ACTION_APP_NOTIFICATION_SETTINGS'
102
+ EXTRA_APP_PACKAGE = 'FACADE_IMPORT_EXTRA_APP_PACKAGE'
103
+ ACTION_APPLICATION_DETAILS_SETTINGS = 'FACADE_IMPORT_ACTION_APPLICATION_DETAILS_SETTINGS'
104
+
105
+
106
+ class Uri:
107
+ def __init__(self, package_name):
108
+ logger.debug("[MOCK] Uri initialized")
109
+
110
+
111
+ class NotificationManager:
112
+ pass
113
+
114
+
115
+ class NotificationChannel:
116
+ def __init__(self, channel_id, channel_name, importance):
117
+ self.description = None
118
+ self.channel_id = channel_id
119
+ self.channel = None
120
+ logger.debug(
121
+ f"[MOCK] NotificationChannel initialized with "
122
+ f"id={channel_id}, name={channel_name}, importance={importance}"
123
+ )
124
+
125
+ def createNotificationChannel(self, channel):
126
+ self.channel = channel
127
+ logger.debug(f"[MOCK] NotificationChannel.createNotificationChannel called with channel={channel}")
128
+
129
+ def getNotificationChannel(self, channel_id):
130
+ self.channel_id = channel_id
131
+ logger.debug(f"[MOCK] NotificationChannel.getNotificationChannel called with id={channel_id}")
132
+
133
+ def setDescription(self, description):
134
+ self.description = description
135
+ logger.debug(f"[MOCK] NotificationChannel.setDescription called with description={description}")
136
+
137
+ def getId(self):
138
+ logger.debug(f"[MOCK] NotificationChannel.getId called, returning {self.channel_id}")
139
+ return self.channel_id
140
+
141
+
142
+ class IconCompat:
143
+ def createWithBitmap(self, bitmap):
144
+ logger.debug(f"[MOCK] IconCompat.createWithBitmap called with bitmap={bitmap}")
145
+
146
+
147
+ class Color:
148
+ def __init__(self):
149
+ logger.debug("[MOCK] Color initialized")
150
+
151
+ def parseColor(self, color: str):
152
+ logger.debug(f"[MOCK] Color.parseColor called with color={color}")
153
+ return self
154
+
155
+
156
+ class RemoteViews:
157
+ def __init__(self, package_name, small_layout_id):
158
+ logger.debug(
159
+ f"[MOCK] RemoteViews initialized with package_name={package_name}, layout_id={small_layout_id}"
160
+ )
161
+
162
+ def createWithBitmap(self, bitmap):
163
+ logger.debug(f"[MOCK] RemoteViews.createWithBitmap called with bitmap={bitmap}")
164
+
165
+ def setTextViewText(self, id, text):
166
+ logger.debug(f"[MOCK] RemoteViews.setTextViewText called with id={id}, text={text}")
167
+
168
+ def setTextColor(self, id, color: Color):
169
+ logger.debug(f"[MOCK] RemoteViews.setTextColor called with id={id}, color={color}")
170
+
171
+
172
+ class NotificationManagerCompat:
173
+ IMPORTANCE_HIGH = 4
174
+ IMPORTANCE_DEFAULT = 3
175
+ IMPORTANCE_LOW = ''
176
+ IMPORTANCE_MIN = ''
177
+ IMPORTANCE_NONE = ''
178
+
179
+
180
+ class NotificationCompat:
181
+ DEFAULT_ALL = 3
182
+ PRIORITY_HIGH = 4
183
+ PRIORITY_DEFAULT = ''
184
+ PRIORITY_LOW = ''
185
+ PRIORITY_MIN = ''
186
+
187
+
188
+ class MActions:
189
+ def clear(self):
190
+ """This Removes all buttons"""
191
+ logger.debug('[MOCK] MActions.clear called')
192
+
193
+
194
+ class NotificationCompatBuilder:
195
+ def __init__(self, context, channel_id):
196
+ self.mActions = MActions()
197
+
198
+ def setProgress(self, max_value, current_value, endless):
199
+ logger.debug(f"[MOCK] setProgress called max={max_value}, current={current_value}, endless={endless}")
200
+
201
+ def setStyle(self, style):
202
+ logger.debug(f"[MOCK] setStyle called with style={style}")
203
+
204
+ def setContentTitle(self, title):
205
+ logger.debug(f"[MOCK] setContentTitle called with title={title}")
206
+
207
+ def setContentText(self, text):
208
+ logger.debug(f"[MOCK] setContentText called with text={text}")
209
+
210
+ def setSmallIcon(self, icon):
211
+ logger.debug(f"[MOCK] setSmallIcon called with icon={icon}")
212
+
213
+ def setLargeIcon(self, icon):
214
+ logger.debug(f"[MOCK] setLargeIcon called with icon={icon}")
215
+
216
+ def setAutoCancel(self, auto_cancel: bool):
217
+ logger.debug(f"[MOCK] setAutoCancel called with auto_cancel={auto_cancel}")
218
+
219
+ def setPriority(self, priority: Importance):
220
+ logger.debug(f"[MOCK] setPriority called with priority={priority}")
221
+
222
+ def setDefaults(self, defaults):
223
+ logger.debug(f"[MOCK] setDefaults called with defaults={defaults}")
224
+
225
+ def setOngoing(self, persistent: bool):
226
+ logger.debug(f"[MOCK] setOngoing called with persistent={persistent}")
227
+
228
+ def setOnlyAlertOnce(self, state):
229
+ logger.debug(f"[MOCK] setOnlyAlertOnce called with state={state}")
230
+
231
+ def build(self):
232
+ logger.debug("[MOCK] build called")
233
+
234
+ def setContentIntent(self, pending_action_intent: PendingIntent):
235
+ logger.debug(f"[MOCK] setContentIntent called with {pending_action_intent}")
236
+
237
+ def addAction(self, icon_int, action_text, pending_action_intent):
238
+ logger.debug(
239
+ f"[MOCK] addAction called with icon={icon_int}, text={action_text}, intent={pending_action_intent}"
240
+ )
241
+
242
+ def setShowWhen(self, state):
243
+ logger.debug(f"[MOCK] setShowWhen called with state={state}")
244
+
245
+ def setWhen(self, time_ms):
246
+ logger.debug(f"[MOCK] setWhen called with time_ms={time_ms}")
247
+
248
+ def setCustomContentView(self, layout):
249
+ logger.debug(f"[MOCK] setCustomContentView called with layout={layout}")
250
+
251
+ def setCustomBigContentView(self, layout):
252
+ logger.debug(f"[MOCK] setCustomBigContentView called with layout={layout}")
253
+
254
+ def setSubText(self, text):
255
+ logger.debug(f"[MOCK] setSubText called with text={text}")
256
+
257
+ def setColor(self, color: Color) -> None:
258
+ logger.debug(f"[MOCK] setColor called with color={color}")
259
+
260
+
261
+ class NotificationCompatBigTextStyle:
262
+ def bigText(self, body):
263
+ logger.debug(f"[MOCK] NotificationCompatBigTextStyle.bigText called with body={body}")
264
+ return self
265
+
266
+
267
+ class NotificationCompatBigPictureStyle:
268
+ def bigPicture(self, bitmap):
269
+ logger.debug(f"[MOCK] NotificationCompatBigPictureStyle.bigPicture called with bitmap={bitmap}")
270
+ return self
271
+
272
+
273
+ class NotificationCompatInboxStyle:
274
+ def addLine(self, line):
275
+ logger.debug(f"[MOCK] NotificationCompatInboxStyle.addLine called with line={line}")
276
+ return self
277
+
278
+
279
+ class NotificationCompatDecoratedCustomViewStyle:
280
+ def __init__(self):
281
+ logger.debug("[MOCK] NotificationCompatDecoratedCustomViewStyle initialized")
282
+
283
+
284
+ class Permission:
285
+ POST_NOTIFICATIONS = str
286
+
287
+
288
+ def check_permission(permission: Permission.POST_NOTIFICATIONS):
289
+ logger.debug(f"[MOCK] check_permission called with {permission}")
290
+
291
+
292
+ def request_permissions(_list: [], _callback):
293
+ logger.debug(f"[MOCK] request_permissions called with {_list}")
294
+ _callback()
295
+
296
+
297
+ class AndroidActivity:
298
+ def bind(self, on_new_intent):
299
+ logger.debug(f"[MOCK] AndroidActivity.bind called with {on_new_intent}")
300
+
301
+ def unbind(self, on_new_intent):
302
+ logger.debug(f"[MOCK] AndroidActivity.unbind called with {on_new_intent}")
303
+
304
+
305
+ class PythonActivity:
306
+ def __init__(self):
307
+ logger.debug("[MOCK] PythonActivity initialized")
308
+
309
+
310
+ class DummyIcon:
311
+ icon = 101
312
+
313
+ def __init__(self):
314
+ logger.debug("[MOCK] DummyIcon initialized")
315
+
316
+
317
+ class Context:
318
+ def __init__(self):
319
+ logger.debug("[MOCK] Context initialized")
320
+
321
+ @staticmethod
322
+ def getApplicationInfo():
323
+ logger.debug("[MOCK] Context.getApplicationInfo called")
324
+ return DummyIcon
325
+
326
+ @staticmethod
327
+ def getResources():
328
+ logger.debug("[MOCK] Context.getResources called")
329
+ return Resources
330
+
331
+ @staticmethod
332
+ def getPackageName():
333
+ logger.debug("[MOCK] Context.getPackageName called")
334
+ return None # TODO get package name from buildozer.spec file
335
+
336
+
337
+ class Resources:
338
+ @staticmethod
339
+ def getIdentifier(layout_name, folder_base, package_name):
340
+ logger.debug(
341
+ f"[MOCK] Resources.getIdentifier called "
342
+ f"layout={layout_name}, folder={folder_base}, package={package_name}"
343
+ )
344
+ return 0
345
+
346
+ def setTextViewText(self, text_id, text):
347
+ logger.debug(f"[MOCK] Resources.setTextViewText called with text_id={text_id}, text={text}")
348
+
349
+
350
+
351
+ class AppWidgetManager:
352
+ @classmethod #imporatnt
353
+ def getInstance(cls, context):
354
+ logger.debug(f"[MOCK] AppWidgetManager got context instance={context}")
355
+ return cls
356
+
357
+ @classmethod #important 1
358
+ def updateAppWidget(cls, widget_ids, java_view_object):
359
+ logger.debug(f"[MOCK] updateAppWidget called with widget_ids={widget_ids}, java_view_object={java_view_object}")
360
+
361
+ @classmethod #important 2
362
+ def getAppWidgetIds(cls,component):
363
+ logger.debug(f"[MOCK] getAppWidgetIds called with component={component}")
364
+
365
+
366
+
367
+ class ComponentName:
368
+ def __init__(self, context='', apk_name=''):
369
+ logger.debug(f"[MOCK] ComponentName initialized with context={context}, apk_name={apk_name}")
370
+
371
+ def getInstance(self,context):
372
+ logger.debug(f"[MOCK] AppWidgetManager got context instance={context}")
373
+ return self
374
+
375
+
376
+
@@ -0,0 +1,11 @@
1
+ from .config import is_platform_android
2
+
3
+ if is_platform_android():
4
+ from jnius import autoclass
5
+ String = autoclass("java.lang.String")
6
+ RemoteViews_ = autoclass('android.widget.RemoteViews')
7
+ AppWidgetManager_ = autoclass('android.appwidget.AppWidgetManager')
8
+ ComponentName = autoclass('android.content.ComponentName')
9
+
10
+ else:
11
+ raise ImportError("Not on Android, Unable to import Java classes")
@@ -0,0 +1,129 @@
1
+ print("----------------------Running android_widget.maker-----------------")
2
+
3
+ from dataclasses import dataclass
4
+ from typing import List, Optional
5
+
6
+
7
+ @dataclass
8
+ class Receiver:
9
+ name: str
10
+ actions: List[str]
11
+ enabled: bool = True
12
+ exported: bool = False
13
+ label: Optional[str] = None
14
+ meta_name: Optional[str] = "android.appwidget.provider"
15
+ meta_resource: Optional[str] = None
16
+
17
+ def to_xml(self, package: str) -> str:
18
+ attrs = [
19
+ f'android:name="{package}.{self.name}"',
20
+ f'android:enabled="{str(self.enabled).lower()}"',
21
+ f'android:exported="{str(self.exported).lower()}"',
22
+ ]
23
+
24
+ if self.label:
25
+ attrs.append(f'android:label="{self.label}"')
26
+
27
+ xml = [f"<receiver {' '.join(attrs)}>"]
28
+ xml.append(" <intent-filter>")
29
+
30
+ for action in self.actions:
31
+ xml.append(f' <action android:name="{action}" />')
32
+
33
+ xml.append(" </intent-filter>")
34
+
35
+ if self.meta_resource:
36
+ xml.append(
37
+ f' <meta-data android:name="{self.meta_name}"\n'
38
+ f' android:resource="{self.meta_resource}" />'
39
+ )
40
+
41
+ xml.append("</receiver>")
42
+ return "\n".join(xml)
43
+
44
+
45
+ def generate_receivers(package: str) -> str:
46
+ receivers = [
47
+ Receiver(
48
+ name="Action1",
49
+ actions=["android.intent.action.BOOT_COMPLETED"],
50
+ ),
51
+ Receiver(
52
+ name="SimpleWidget",
53
+ label="Simple Text",
54
+ actions=["android.appwidget.action.APPWIDGET_UPDATE"],
55
+ meta_resource="@xml/widgetproviderinfo",
56
+ ),
57
+ Receiver(
58
+ name="ButtonWidget",
59
+ label="Counter Button Demo",
60
+ actions=["android.appwidget.action.APPWIDGET_UPDATE"],
61
+ meta_resource="@xml/button_widget_provider",
62
+ ),
63
+ Receiver(
64
+ name="Image1",
65
+ actions=[
66
+ "android.intent.action.BOOT_COMPLETED",
67
+ "android.appwidget.action.APPWIDGET_UPDATE",
68
+ ],
69
+ meta_resource="@xml/image_test_widget_info",
70
+ ),
71
+ ]
72
+
73
+ return "\n\n".join(r.to_xml(package) for r in receivers)
74
+
75
+
76
+
77
+ print("------------------ Hello from android_widget.service_injector ------------------")
78
+
79
+
80
+ def inject_foreground_service_types(
81
+ manifest_text: str,
82
+ package: str,
83
+ services: dict[str, str],
84
+ ) -> str:
85
+ """
86
+ Inject android:foregroundServiceType into <service /> tags.
87
+ :param manifest_text: AndroidManifest.xml file Text Content
88
+ :param package: package.domain + "." + package.name
89
+ :param services:
90
+ {
91
+ "service_name": "service_type",
92
+ "music": "mediaPlayback",
93
+ "location": "location"
94
+ }
95
+ """
96
+
97
+ for name, fgs_type in services.items():
98
+ service_name = f"{package}.Service{name.capitalize()}"
99
+ target = f'android:name="{service_name}"'
100
+
101
+ pos = manifest_text.find(target)
102
+ if pos == -1:
103
+ print(f"Error_101: {service_name} not found in manifest")
104
+ continue
105
+
106
+ # Find the end of the <service ... /> tag
107
+ end = manifest_text.find("/>", pos)
108
+ if end == -1:
109
+ print(f"Error_101: {service_name} found but no '/>' closing tag")
110
+ continue
111
+
112
+ segment = manifest_text[pos:end]
113
+
114
+ if "android:foregroundServiceType=" in segment:
115
+ print(f"Error_101: {service_name} already has foregroundServiceType")
116
+ continue
117
+
118
+ manifest_text = (
119
+ manifest_text[:end]
120
+ + f' android:foregroundServiceType="{fgs_type}"'
121
+ + manifest_text[end:]
122
+ )
123
+
124
+ print(
125
+ f"Successfully_101: Added foregroundServiceType='{fgs_type}' "
126
+ f"to {service_name}"
127
+ )
128
+
129
+ return manifest_text
@@ -0,0 +1,43 @@
1
+ from pathlib import Path
2
+
3
+
4
+ class SpecFile:
5
+ def __init__(self, path: str):
6
+ self.path = Path(path)
7
+ self.data = {}
8
+ self._parse()
9
+
10
+ def _parse(self):
11
+ current_section = None
12
+
13
+ for raw_line in self.path.read_text().splitlines():
14
+ line = raw_line.strip()
15
+
16
+ # skip empty lines and full-line comments
17
+ if not line or line.startswith("#"):
18
+ continue
19
+
20
+ # section header
21
+ if line.startswith("[") and line.endswith("]"):
22
+ current_section = line[1:-1].strip()
23
+ self.data.setdefault(current_section, {})
24
+ continue
25
+
26
+ # key = value lines
27
+ if "=" in line and current_section:
28
+ key, value = line.split("=", 1)
29
+
30
+ key = key.strip()
31
+ value = value.strip()
32
+
33
+ # ignore commented keys like "# key = value"
34
+ if key.startswith("#"):
35
+ continue
36
+
37
+ self.data[current_section][key] = value
38
+
39
+ def get(self, section: str, key: str, default=None):
40
+ return self.data.get(section, {}).get(key, default)
41
+
42
+ def section(self, section: str) -> dict:
43
+ return self.data.get(section, {}).copy()
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: android-widgets
3
- Version: 0.1.0
4
- Summary: A simple hello world package for Kivy Android widgets.
3
+ Version: 0.1.2
4
+ Summary: This would be used to auto gen XMLs for Android Home Screen widgets.
5
5
  Author-email: Fabian <fector101@yahoo.com>
6
6
  License: MIT
7
- Project-URL: Homepage, https://github.com/yourusername/kivy-androidwidgets
8
- Project-URL: Documentation, https://github.com/yourusername/kivy-androidwidgets
9
- Project-URL: Source, https://github.com/yourusername/kivy-androidwidgets
10
- Project-URL: Tracker, https://github.com/yourusername/kivy-androidwidgets/issues
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
11
  Keywords: kivy,android,widgets,hello-world,python-package
12
12
  Classifier: Programming Language :: Python :: 3
13
13
  Classifier: License :: OSI Approved :: MIT License
@@ -27,7 +27,7 @@ Complete Sample: [working app](https://github.com/Fector101/wallpaper-carousel)
27
27
  5 Steps For a simple widget
28
28
  ---
29
29
 
30
- step 1: First you Design How you want the Widget to Look [it's Layout].
30
+ ### step 1: First you Design How you want the Widget to Look [it's Layout].
31
31
  Store it in: `res/layout/simple_widget.xml`
32
32
  This a simple widget with a text
33
33
  ```xml
@@ -50,7 +50,7 @@ This a simple widget with a text
50
50
 
51
51
  </LinearLayout>
52
52
  ```
53
- step 2: Create an xml containing the info about the widget.
53
+ ### step 2: Create an xml containing the info about the widget.
54
54
  Like: size, preview icon and others
55
55
  path: `res/xml/widgetproviderinfo.xml`
56
56
  ```xml
@@ -66,7 +66,7 @@ path: `res/xml/widgetproviderinfo.xml`
66
66
  ```
67
67
  Create preview image png in right path`res/drawable/ic_launcher_foreground.png`
68
68
 
69
- step 3: Create a `AppWidgetProvider` it's used to receive events for widget.
69
+ ### step 3: Create a `AppWidgetProvider` it's used to receive events for widget.
70
70
  path: `src/SimpleWidget.java`.
71
71
  This will receive an event when widget is add to change it's text
72
72
 
@@ -99,7 +99,8 @@ public class SimpleWidget extends AppWidgetProvider {
99
99
  }
100
100
  }
101
101
  ```
102
- Step 4: Automate injecting Receiver in XML
102
+
103
+ ### Step 4: Automate injecting Receiver in XML
103
104
 
104
105
  path:`p4a/hook.py`
105
106
 
@@ -138,16 +139,19 @@ def after_apk_build(toolchain: ToolchainCL):
138
139
  print("Successfully_101: Manifest update completed successfully!")
139
140
 
140
141
  ```
141
- Step 5: From `buildozer.spec` tell it you want to add resources, src and p4a hook
142
+
143
+ ### Step 5: From `buildozer.spec` tell it you want to add resources, src and p4a hook
142
144
  ```ini
143
145
  android.add_resources = res
144
146
  android.add_src = src
145
147
  p4a.hook = p4a/hook.py
146
148
  ```
147
149
 
148
- For More widget customisation check: [How to Customise.md](how-to-customise.md)
149
- ---
150
150
 
151
151
  Sample Image:
152
152
 
153
153
  ![Rounded corners widget](https://raw.githubusercontent.com/Fector101/kivy-androidwidgets/main/imgs/not-rounded.jpg)
154
+
155
+
156
+ ### For More widget customisation check: [How to Customise.md](how-to-customise.md)
157
+
@@ -0,0 +1,14 @@
1
+ README.md
2
+ pyproject.toml
3
+ android_widgets/__init__.py
4
+ android_widgets/aw_logging.py
5
+ android_widgets/config.py
6
+ android_widgets/facade.py
7
+ android_widgets/java_classes.py
8
+ android_widgets/maker.py
9
+ android_widgets/tools.py
10
+ android_widgets.egg-info/PKG-INFO
11
+ android_widgets.egg-info/SOURCES.txt
12
+ android_widgets.egg-info/dependency_links.txt
13
+ android_widgets.egg-info/requires.txt
14
+ android_widgets.egg-info/top_level.txt
@@ -0,0 +1,3 @@
1
+ android_widgets
2
+ dist
3
+ imgs
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "android-widgets"
7
- version = "0.1.0"
8
- description = "A simple hello world package for Kivy Android widgets."
7
+ version = "0.1.2"
8
+ description = "This would be used to auto gen XMLs for Android Home Screen widgets."
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  authors = [
11
11
  { name = "Fabian", email = "fector101@yahoo.com" }
@@ -32,10 +32,10 @@ classifiers = [
32
32
  ]
33
33
 
34
34
  [project.urls]
35
- Homepage = "https://github.com/yourusername/kivy-androidwidgets"
36
- Documentation = "https://github.com/yourusername/kivy-androidwidgets"
37
- Source = "https://github.com/yourusername/kivy-androidwidgets"
38
- Tracker = "https://github.com/yourusername/kivy-androidwidgets/issues"
35
+ Homepage = "https://github.com/Fector101/kivy-androidwidgets"
36
+ Documentation = "https://github.com/Fector101/kivy-androidwidgets"
37
+ Source = "https://github.com/Fector101/kivy-androidwidgets"
38
+ Tracker = "https://github.com/Fector101/kivy-androidwidgets/issues"
39
39
 
40
40
  [tool.setuptools.packages.find]
41
41
  where = ["."]
@@ -1,5 +0,0 @@
1
- def hello():
2
- """
3
- Simple hello function for testing the package.
4
- """
5
- return "Hello from android-widgets!"
@@ -1,8 +0,0 @@
1
- README.md
2
- pyproject.toml
3
- android-widgets/__init__.py
4
- android_widgets.egg-info/PKG-INFO
5
- android_widgets.egg-info/SOURCES.txt
6
- android_widgets.egg-info/dependency_links.txt
7
- android_widgets.egg-info/requires.txt
8
- android_widgets.egg-info/top_level.txt
@@ -1,3 +0,0 @@
1
- android-widgets
2
- dist
3
- imgs