├── .gitignore ├── README.md ├── bin └── kivyalarm-0.1-armeabi-v7a-debug.apk ├── buildozer.spec ├── java_src ├── AndroidManifest.tmpl.xml ├── KivyAlarmReceiver.java └── PythonActivity.java ├── main.py └── service └── main.py /.gitignore: -------------------------------------------------------------------------------- 1 | /.buildozer -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android alarm manager and service with Kivy 2 | 3 | Simple demonstration of how to build an app using Andoid's alarm manager and service implemented in Kivy 4 | 5 | ## How to? 6 | 7 | Inside your project locate PythonActivity.java it should be inside 8 | 9 | ```.buildozer/android/platform/python-for-android/pythonforandroid/bootstraps/sdl2/build/src/main/java/org/kivy/android/PythonActivity.java``` 10 | 11 | and replace it with one from java_scr folder, then add KivyAlarmReceiver.java to the same folder where PythonActivity.java is, then replace AndroidManifest.tmpl.xml 12 | 13 | it should be inside ```.buildozer/android/platform/python-for-android/pythonforandroid/bootstraps/sdl2/build/templates/AndroidManifest.tmpl.xml``` with the one from java_scr folder 14 | 15 | then check buildozer.spec file or simply copy it. 16 | 17 | If you've alredy built some app before you should ```buildozer android clean``` before building your app again with this changes. 18 | 19 | Already built app is in the bin folder. 20 | -------------------------------------------------------------------------------- /bin/kivyalarm-0.1-armeabi-v7a-debug.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adywizard/kivy_alarm_manager_with_service/ee5d525925c553a973b4411688fd0ffb813ef369/bin/kivyalarm-0.1-armeabi-v7a-debug.apk -------------------------------------------------------------------------------- /buildozer.spec: -------------------------------------------------------------------------------- 1 | [app] 2 | 3 | # (str) Title of your application 4 | title = Kivy Alarm 5 | 6 | # (str) Package name 7 | package.name = kivyalarm 8 | 9 | # (str) Package domain (needed for android/ios packaging) 10 | package.domain = org.test 11 | 12 | # (str) Source code where the main.py live 13 | source.dir = . 14 | 15 | # (list) Source files to include (let empty to include all the files) 16 | source.include_exts = py,png,jpg,kv,atlas 17 | 18 | # (list) List of inclusions using pattern matching 19 | #source.include_patterns = assets/*,images/*.png 20 | 21 | # (list) Source files to exclude (let empty to not exclude anything) 22 | source.exclude_exts = spec 23 | 24 | # (list) List of directory to exclude (let empty to not exclude anything) 25 | source.exclude_dirs = tests, bin, venv 26 | 27 | # (list) List of exclusions using pattern matching 28 | #source.exclude_patterns = license,images/*/*.jpg 29 | 30 | # (str) Application versioning (method 1) 31 | version = 0.1 32 | 33 | # (str) Application versioning (method 2) 34 | # version.regex = __version__ = ['"](.*)['"] 35 | # version.filename = %(source.dir)s/main.py 36 | 37 | # (list) Application requirements 38 | # comma separated e.g. requirements = sqlite3,kivy 39 | requirements = python3,kivy==2.0.0 40 | 41 | # (str) Custom source folders for requirements 42 | # Sets custom source for any requirements with recipes 43 | # requirements.source.kivy = ../../kivy 44 | 45 | # (str) Presplash of the application 46 | #presplash.filename = %(source.dir)s/data/presplash.png 47 | 48 | # (str) Icon of the application 49 | #icon.filename = %(source.dir)s/data/icon.png 50 | 51 | # (str) Supported orientation (one of landscape, sensorLandscape, portrait or all) 52 | orientation = portrait 53 | 54 | # (list) List of service to declare 55 | #services = NAME:ENTRYPOINT_TO_PY,NAME2:ENTRYPOINT2_TO_PY 56 | 57 | # 58 | # OSX Specific 59 | # 60 | 61 | # 62 | # author = © Copyright Info 63 | 64 | # change the major version of python used by the app 65 | osx.python_version = 3 66 | 67 | # Kivy version to use 68 | osx.kivy_version = 1.9.1 69 | 70 | # 71 | # Android specific 72 | # 73 | 74 | # (bool) Indicate if the application should be fullscreen or not 75 | fullscreen = 0 76 | 77 | # (string) Presplash background color (for android toolchain) 78 | # Supported formats are: #RRGGBB #AARRGGBB or one of the following names: 79 | # red, blue, green, black, white, gray, cyan, magenta, yellow, lightgray, 80 | # darkgray, grey, lightgrey, darkgrey, aqua, fuchsia, lime, maroon, navy, 81 | # olive, purple, silver, teal. 82 | #android.presplash_color = #FFFFFF 83 | 84 | # (string) Presplash animation using Lottie format. 85 | # see https://lottiefiles.com/ for examples and https://airbnb.design/lottie/ 86 | # for general documentation. 87 | # Lottie files can be created using various tools, like Adobe After Effect or Synfig. 88 | #android.presplash_lottie = "path/to/lottie/file.json" 89 | 90 | # (list) Permissions 91 | android.permissions = INTERNET,RECEIVE_BOOT_COMPLETED,FOREGROUND_SERVICE 92 | 93 | # (list) features (adds uses-feature -tags to manifest) 94 | #android.features = android.hardware.usb.host 95 | 96 | # (int) Target Android API, should be as high as possible. 97 | android.api = 29 98 | 99 | # (int) Minimum API your APK will support. 100 | #android.minapi = 21 101 | 102 | # (int) Android SDK version to use 103 | #android.sdk = 20 104 | 105 | # (str) Android NDK version to use 106 | #android.ndk = 19b 107 | 108 | # (int) Android NDK API to use. This is the minimum API your app will support, it should usually match android.minapi. 109 | #android.ndk_api = 21 110 | 111 | # (bool) Use --private data storage (True) or --dir public storage (False) 112 | #android.private_storage = True 113 | 114 | # (str) Android NDK directory (if empty, it will be automatically downloaded.) 115 | #android.ndk_path = 116 | 117 | # (str) Android SDK directory (if empty, it will be automatically downloaded.) 118 | #android.sdk_path = 119 | 120 | # (str) ANT directory (if empty, it will be automatically downloaded.) 121 | #android.ant_path = 122 | 123 | # (bool) If True, then skip trying to update the Android sdk 124 | # This can be useful to avoid excess Internet downloads or save time 125 | # when an update is due and you just want to test/build your package 126 | android.skip_update = True 127 | 128 | # (bool) If True, then automatically accept SDK license 129 | # agreements. This is intended for automation only. If set to False, 130 | # the default, you will be shown the license when first running 131 | # buildozer. 132 | android.accept_sdk_license = True 133 | 134 | # (str) Android entry point, default is ok for Kivy-based app 135 | #android.entrypoint = org.renpy.android.PythonActivity 136 | 137 | # (str) Android app theme, default is ok for Kivy-based app 138 | # android.apptheme = "@android:style/Theme.NoTitleBar" 139 | 140 | # (list) Pattern to whitelist for the whole project 141 | #android.whitelist = 142 | 143 | # (str) Path to a custom whitelist file 144 | #android.whitelist_src = 145 | 146 | # (str) Path to a custom blacklist file 147 | #android.blacklist_src = 148 | 149 | # (list) List of Java .jar files to add to the libs so that pyjnius can access 150 | # their classes. Don't add jars that you do not need, since extra jars can slow 151 | # down the build process. Allows wildcards matching, for example: 152 | # OUYA-ODK/libs/*.jar 153 | #android.add_jars = foo.jar,bar.jar,path/to/more/*.jar 154 | 155 | # (list) List of Java files to add to the android project (can be java or a 156 | # directory containing the files) 157 | #android.add_src = 158 | 159 | # (list) Android AAR archives to add 160 | #android.add_aars = 161 | 162 | # (list) Gradle dependencies to add 163 | #android.gradle_dependencies = 164 | 165 | # (list) add java compile options 166 | # this can for example be necessary when importing certain java libraries using the 'android.gradle_dependencies' option 167 | # see https://developer.android.com/studio/write/java8-support for further information 168 | # android.add_compile_options = "sourceCompatibility = 1.8", "targetCompatibility = 1.8" 169 | 170 | # (list) Gradle repositories to add {can be necessary for some android.gradle_dependencies} 171 | # please enclose in double quotes 172 | # e.g. android.gradle_repositories = "maven { url 'https://kotlin.bintray.com/ktor' }" 173 | #android.add_gradle_repositories = 174 | 175 | # (list) packaging options to add 176 | # see https://google.github.io/android-gradle-dsl/current/com.android.build.gradle.internal.dsl.PackagingOptions.html 177 | # can be necessary to solve conflicts in gradle_dependencies 178 | # please enclose in double quotes 179 | # e.g. android.add_packaging_options = "exclude 'META-INF/common.kotlin_module'", "exclude 'META-INF/*.kotlin_module'" 180 | #android.add_packaging_options = 181 | 182 | # (list) Java classes to add as activities to the manifest. 183 | #android.add_activities = com.example.ExampleActivity 184 | 185 | # (str) OUYA Console category. Should be one of GAME or APP 186 | # If you leave this blank, OUYA support will not be enabled 187 | #android.ouya.category = GAME 188 | 189 | # (str) Filename of OUYA Console icon. It must be a 732x412 png image. 190 | #android.ouya.icon.filename = %(source.dir)s/data/ouya_icon.png 191 | 192 | # (str) XML file to include as an intent filters in tag 193 | #android.manifest.intent_filters = 194 | 195 | # (str) launchMode to set for the main activity 196 | #android.manifest.launch_mode = standard 197 | 198 | # (list) Android additional libraries to copy into libs/armeabi 199 | #android.add_libs_armeabi = libs/android/*.so 200 | #android.add_libs_armeabi_v7a = libs/android-v7/*.so 201 | #android.add_libs_arm64_v8a = libs/android-v8/*.so 202 | #android.add_libs_x86 = libs/android-x86/*.so 203 | #android.add_libs_mips = libs/android-mips/*.so 204 | 205 | # (bool) Indicate whether the screen should stay on 206 | # Don't forget to add the WAKE_LOCK permission if you set this to True 207 | #android.wakelock = False 208 | 209 | # (list) Android application meta-data to set (key=value format) 210 | #android.meta_data = 211 | 212 | # (list) Android library project to add (will be added in the 213 | # project.properties automatically.) 214 | #android.library_references = 215 | 216 | # (list) Android shared libraries which will be added to AndroidManifest.xml using tag 217 | #android.uses_library = 218 | 219 | # (str) Android logcat filters to use 220 | android.logcat_filters = *:S python:D 221 | 222 | # (bool) Copy library instead of making a libpymodules.so 223 | #android.copy_libs = 1 224 | 225 | # (str) The Android arch to build for, choices: armeabi-v7a, arm64-v8a, x86, x86_64 226 | android.arch = armeabi-v7a 227 | 228 | # (int) overrides automatic versionCode computation (used in build.gradle) 229 | # this is not the same as app version and should only be edited if you know what you're doing 230 | # android.numeric_version = 1 231 | 232 | # (bool) enables Android auto backup feature (Android API >=23) 233 | android.allow_backup = True 234 | 235 | # (str) XML file for custom backup rules (see official auto backup documentation) 236 | # android.backup_rules = 237 | 238 | # 239 | # Python for android (p4a) specific 240 | # 241 | 242 | # (str) python-for-android fork to use, defaults to upstream (kivy) 243 | #p4a.fork = kivy 244 | 245 | # (str) python-for-android branch to use, defaults to master 246 | #p4a.branch = master 247 | 248 | # (str) python-for-android git clone directory (if empty, it will be automatically cloned from github) 249 | #p4a.source_dir = 250 | 251 | # (str) The directory in which python-for-android should look for your own build recipes (if any) 252 | #p4a.local_recipes = 253 | 254 | # (str) Filename to the hook for p4a 255 | #p4a.hook = 256 | 257 | # (str) Bootstrap to use for android builds 258 | p4a.bootstrap = sdl2 259 | 260 | # (int) port number to specify an explicit --port= p4a argument (eg for bootstrap flask) 261 | #p4a.port = 262 | 263 | # Control passing the --use-setup-py vs --ignore-setup-py to p4a 264 | # "in the future" --use-setup-py is going to be the default behaviour in p4a, right now it is not 265 | # Setting this to false will pass --ignore-setup-py, true will pass --use-setup-py 266 | # NOTE: this is general setuptools integration, having pyproject.toml is enough, no need to generate 267 | # setup.py if you're using Poetry, but you need to add "toml" to source.include_exts. 268 | #p4a.setup_py = false 269 | 270 | 271 | # 272 | # iOS specific 273 | # 274 | 275 | # (str) Path to a custom kivy-ios folder 276 | #ios.kivy_ios_dir = ../kivy-ios 277 | # Alternately, specify the URL and branch of a git checkout: 278 | ios.kivy_ios_url = https://github.com/kivy/kivy-ios 279 | ios.kivy_ios_branch = master 280 | 281 | # Another platform dependency: ios-deploy 282 | # Uncomment to use a custom checkout 283 | #ios.ios_deploy_dir = ../ios_deploy 284 | # Or specify URL and branch 285 | ios.ios_deploy_url = https://github.com/phonegap/ios-deploy 286 | ios.ios_deploy_branch = 1.10.0 287 | 288 | # (bool) Whether or not to sign the code 289 | ios.codesign.allowed = false 290 | 291 | # (str) Name of the certificate to use for signing the debug version 292 | # Get a list of available identities: buildozer ios list_identities 293 | #ios.codesign.debug = "iPhone Developer: ()" 294 | 295 | # (str) Name of the certificate to use for signing the release version 296 | #ios.codesign.release = %(ios.codesign.debug)s 297 | 298 | 299 | [buildozer] 300 | 301 | # (int) Log level (0 = error only, 1 = info, 2 = debug (with command output)) 302 | log_level = 2 303 | 304 | # (int) Display warning if buildozer is run as root (0 = False, 1 = True) 305 | warn_on_root = 1 306 | 307 | # (str) Path to build artifact storage, absolute or relative to spec file 308 | # build_dir = ./.buildozer 309 | 310 | # (str) Path to build output (i.e. .apk, .ipa) storage 311 | # bin_dir = ./bin 312 | 313 | # ----------------------------------------------------------------------------- 314 | # List as sections 315 | # 316 | # You can define all the "list" as [section:key]. 317 | # Each line will be considered as a option to the list. 318 | # Let's take [app] / source.exclude_patterns. 319 | # Instead of doing: 320 | # 321 | #[app] 322 | #source.exclude_patterns = license,data/audio/*.wav,data/images/original/* 323 | # 324 | # This can be translated into: 325 | # 326 | #[app:source.exclude_patterns] 327 | #license 328 | #data/audio/*.wav 329 | #data/images/original/* 330 | # 331 | 332 | 333 | # ----------------------------------------------------------------------------- 334 | # Profiles 335 | # 336 | # You can extend section / key with a profile 337 | # For example, you want to deploy a demo version of your application without 338 | # HD content. You could first change the title to add "(demo)" in the name 339 | # and extend the excluded directories to remove the HD content. 340 | # 341 | #[app@demo] 342 | #title = My Application (demo) 343 | # 344 | #[app:source.exclude_patterns@demo] 345 | #images/hd/* 346 | # 347 | # Then, invoke the command line with the "demo" profile: 348 | # 349 | #buildozer --profile demo android debug 350 | -------------------------------------------------------------------------------- /java_src/AndroidManifest.tmpl.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 10 | 11 | = 9 %} 17 | android:xlargeScreens="true" 18 | {% endif %} 19 | /> 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | {% for perm in args.permissions %} 30 | {% if '.' in perm %} 31 | 32 | {% else %} 33 | 34 | {% endif %} 35 | {% endfor %} 36 | 37 | {% if args.wakelock %} 38 | 39 | {% endif %} 40 | 41 | {% if args.billing_pubkey %} 42 | 43 | {% endif %} 44 | 45 | {{ args.extra_manifest_xml }} 46 | 47 | 48 | 57 | 63 | {% for l in args.android_used_libs %} 64 | 65 | {% endfor %} 66 | 67 | {% for m in args.meta_data %} 68 | {% endfor %} 69 | 70 | 71 | 79 | 80 | {% if args.launcher %} 81 | 82 | 83 | 84 | 85 | 86 | {% else %} 87 | 88 | 89 | 90 | 91 | {% endif %} 92 | 93 | {%- if args.intent_filters -%} 94 | {{- args.intent_filters -}} 95 | {%- endif -%} 96 | 97 | 98 | {% if args.launcher %} 99 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | {% endif %} 110 | 111 | {% if service or args.launcher %} 112 | 114 | {% endif %} 115 | {% for name in service_names %} 116 | 118 | {% endfor %} 119 | {% for name in native_services %} 120 | 121 | {% endfor %} 122 | 123 | {% if args.billing_pubkey %} 124 | 126 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | {% endif %} 135 | {% for a in args.add_activity %} 136 | 137 | {% endfor %} 138 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /java_src/KivyAlarmReceiver.java: -------------------------------------------------------------------------------- 1 | package org.kivy.android; 2 | 3 | import android.content.BroadcastReceiver; 4 | import android.content.Intent; 5 | import android.content.Context; 6 | 7 | public class KivyAlarmReceiver extends BroadcastReceiver{ 8 | 9 | @Override 10 | public void onReceive(Context context, Intent intent) { 11 | context.sendBroadcast(new Intent("KIVY_ALARM_SERVICE")); 12 | } 13 | } -------------------------------------------------------------------------------- /java_src/PythonActivity.java: -------------------------------------------------------------------------------- 1 | package org.kivy.android; 2 | 3 | import java.io.InputStream; 4 | import java.io.FileWriter; 5 | import java.io.File; 6 | import java.io.IOException; 7 | import java.lang.reflect.InvocationTargetException; 8 | import java.util.ArrayList; 9 | import java.util.Collections; 10 | import java.util.Iterator; 11 | import java.util.List; 12 | import java.util.Timer; 13 | import java.util.TimerTask; 14 | 15 | import android.app.Activity; 16 | import android.content.Context; 17 | import android.content.Intent; 18 | import android.content.IntentFilter; 19 | import android.content.BroadcastReceiver; 20 | import android.content.pm.ActivityInfo; 21 | import android.content.pm.PackageManager; 22 | import android.graphics.Bitmap; 23 | import android.graphics.BitmapFactory; 24 | import android.graphics.Color; 25 | import android.graphics.PixelFormat; 26 | import android.os.AsyncTask; 27 | import android.os.Bundle; 28 | import android.os.PowerManager; 29 | import android.util.Log; 30 | import android.view.SurfaceView; 31 | import android.view.ViewGroup; 32 | import android.widget.ImageView; 33 | import android.widget.Toast; 34 | 35 | import org.libsdl.app.SDLActivity; 36 | 37 | import org.kivy.android.launcher.Project; 38 | 39 | import org.renpy.android.ResourceManager; 40 | 41 | 42 | public class PythonActivity extends SDLActivity { 43 | private static final String TAG = "PythonActivity"; 44 | 45 | public static PythonActivity mActivity = null; 46 | 47 | private ResourceManager resourceManager = null; 48 | private Bundle mMetaData = null; 49 | private PowerManager.WakeLock mWakeLock = null; 50 | 51 | public String getAppRoot() { 52 | String app_root = getFilesDir().getAbsolutePath() + "/app"; 53 | return app_root; 54 | } 55 | 56 | @Override 57 | protected void onCreate(Bundle savedInstanceState) { 58 | Log.v(TAG, "PythonActivity onCreate running"); 59 | resourceManager = new ResourceManager(this); 60 | 61 | Log.v(TAG, "About to do super onCreate"); 62 | super.onCreate(savedInstanceState); 63 | Log.v(TAG, "Did super onCreate"); 64 | 65 | this.mActivity = this; 66 | this.showLoadingScreen(); 67 | this.registerReceiver(broadcastReceiver, new IntentFilter("KIVY_ALARM_SERVICE")); 68 | 69 | new UnpackFilesTask().execute(getAppRoot()); 70 | } 71 | 72 | BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { 73 | @Override 74 | public void onReceive(Context context, Intent intent) { 75 | String t = "Test"; 76 | String d = "broadcast"; 77 | String a = ""; 78 | start_service(t, d, a); 79 | } 80 | }; 81 | 82 | @Override 83 | protected void onDestroy() { 84 | super.onDestroy(); 85 | unregisterReceiver(broadcastReceiver); 86 | } 87 | 88 | public void loadLibraries() { 89 | String app_root = new String(getAppRoot()); 90 | File app_root_file = new File(app_root); 91 | PythonUtil.loadLibraries(app_root_file, 92 | new File(getApplicationInfo().nativeLibraryDir)); 93 | } 94 | 95 | /** 96 | * Show an error using a toast. (Only makes sense from non-UI 97 | * threads.) 98 | */ 99 | public void toastError(final String msg) { 100 | 101 | final Activity thisActivity = this; 102 | 103 | runOnUiThread(new Runnable () { 104 | public void run() { 105 | Toast.makeText(thisActivity, msg, Toast.LENGTH_LONG).show(); 106 | } 107 | }); 108 | 109 | // Wait to show the error. 110 | synchronized (this) { 111 | try { 112 | this.wait(1000); 113 | } catch (InterruptedException e) { 114 | } 115 | } 116 | } 117 | 118 | private class UnpackFilesTask extends AsyncTask { 119 | @Override 120 | protected String doInBackground(String... params) { 121 | File app_root_file = new File(params[0]); 122 | Log.v(TAG, "Ready to unpack"); 123 | PythonActivityUtil pythonActivityUtil = new PythonActivityUtil(mActivity, resourceManager); 124 | pythonActivityUtil.unpackData("private", app_root_file); 125 | return null; 126 | } 127 | 128 | @Override 129 | protected void onPostExecute(String result) { 130 | // Figure out the directory where the game is. If the game was 131 | // given to us via an intent, then we use the scheme-specific 132 | // part of that intent to determine the file to launch. We 133 | // also use the android.txt file to determine the orientation. 134 | // 135 | // Otherwise, we use the public data, if we have it, or the 136 | // private data if we do not. 137 | mActivity.finishLoad(); 138 | 139 | // finishLoad called setContentView with the SDL view, which 140 | // removed the loading screen. However, we still need it to 141 | // show until the app is ready to render, so pop it back up 142 | // on top of the SDL view. 143 | mActivity.showLoadingScreen(); 144 | 145 | String app_root_dir = getAppRoot(); 146 | if (getIntent() != null && getIntent().getAction() != null && 147 | getIntent().getAction().equals("org.kivy.LAUNCH")) { 148 | File path = new File(getIntent().getData().getSchemeSpecificPart()); 149 | 150 | Project p = Project.scanDirectory(path); 151 | String entry_point = getEntryPoint(p.dir); 152 | SDLActivity.nativeSetenv("ANDROID_ENTRYPOINT", p.dir + "/" + entry_point); 153 | SDLActivity.nativeSetenv("ANDROID_ARGUMENT", p.dir); 154 | SDLActivity.nativeSetenv("ANDROID_APP_PATH", p.dir); 155 | 156 | if (p != null) { 157 | if (p.landscape) { 158 | setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); 159 | } else { 160 | setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); 161 | } 162 | } 163 | 164 | // Let old apps know they started. 165 | try { 166 | FileWriter f = new FileWriter(new File(path, ".launch")); 167 | f.write("started"); 168 | f.close(); 169 | } catch (IOException e) { 170 | // pass 171 | } 172 | } else { 173 | String entry_point = getEntryPoint(app_root_dir); 174 | SDLActivity.nativeSetenv("ANDROID_ENTRYPOINT", entry_point); 175 | SDLActivity.nativeSetenv("ANDROID_ARGUMENT", app_root_dir); 176 | SDLActivity.nativeSetenv("ANDROID_APP_PATH", app_root_dir); 177 | } 178 | 179 | String mFilesDirectory = mActivity.getFilesDir().getAbsolutePath(); 180 | Log.v(TAG, "Setting env vars for start.c and Python to use"); 181 | SDLActivity.nativeSetenv("ANDROID_PRIVATE", mFilesDirectory); 182 | SDLActivity.nativeSetenv("ANDROID_UNPACK", app_root_dir); 183 | SDLActivity.nativeSetenv("PYTHONHOME", app_root_dir); 184 | SDLActivity.nativeSetenv("PYTHONPATH", app_root_dir + ":" + app_root_dir + "/lib"); 185 | SDLActivity.nativeSetenv("PYTHONOPTIMIZE", "2"); 186 | 187 | try { 188 | Log.v(TAG, "Access to our meta-data..."); 189 | mActivity.mMetaData = mActivity.getPackageManager().getApplicationInfo( 190 | mActivity.getPackageName(), PackageManager.GET_META_DATA).metaData; 191 | 192 | PowerManager pm = (PowerManager) mActivity.getSystemService(Context.POWER_SERVICE); 193 | if ( mActivity.mMetaData.getInt("wakelock") == 1 ) { 194 | mActivity.mWakeLock = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "Screen On"); 195 | mActivity.mWakeLock.acquire(); 196 | } 197 | if ( mActivity.mMetaData.getInt("surface.transparent") != 0 ) { 198 | Log.v(TAG, "Surface will be transparent."); 199 | getSurface().setZOrderOnTop(true); 200 | getSurface().getHolder().setFormat(PixelFormat.TRANSPARENT); 201 | } else { 202 | Log.i(TAG, "Surface will NOT be transparent"); 203 | } 204 | } catch (PackageManager.NameNotFoundException e) { 205 | } 206 | 207 | // Launch app if that hasn't been done yet: 208 | if (mActivity.mHasFocus && ( 209 | // never went into proper resume state: 210 | mActivity.mCurrentNativeState == NativeState.INIT || 211 | ( 212 | // resumed earlier but wasn't ready yet 213 | mActivity.mCurrentNativeState == NativeState.RESUMED && 214 | mActivity.mSDLThread == null 215 | ))) { 216 | // Because sometimes the app will get stuck here and never 217 | // actually run, ensure that it gets launched if we're active: 218 | mActivity.onResume(); 219 | } 220 | } 221 | 222 | @Override 223 | protected void onPreExecute() { 224 | } 225 | 226 | @Override 227 | protected void onProgressUpdate(Void... values) { 228 | } 229 | } 230 | 231 | public static ViewGroup getLayout() { 232 | return mLayout; 233 | } 234 | 235 | public static SurfaceView getSurface() { 236 | return mSurface; 237 | } 238 | 239 | //---------------------------------------------------------------------------- 240 | // Listener interface for onNewIntent 241 | // 242 | 243 | public interface NewIntentListener { 244 | void onNewIntent(Intent intent); 245 | } 246 | 247 | private List newIntentListeners = null; 248 | 249 | public void registerNewIntentListener(NewIntentListener listener) { 250 | if ( this.newIntentListeners == null ) 251 | this.newIntentListeners = Collections.synchronizedList(new ArrayList()); 252 | this.newIntentListeners.add(listener); 253 | } 254 | 255 | public void unregisterNewIntentListener(NewIntentListener listener) { 256 | if ( this.newIntentListeners == null ) 257 | return; 258 | this.newIntentListeners.remove(listener); 259 | } 260 | 261 | @Override 262 | protected void onNewIntent(Intent intent) { 263 | if ( this.newIntentListeners == null ) 264 | return; 265 | this.onResume(); 266 | synchronized ( this.newIntentListeners ) { 267 | Iterator iterator = this.newIntentListeners.iterator(); 268 | while ( iterator.hasNext() ) { 269 | (iterator.next()).onNewIntent(intent); 270 | } 271 | } 272 | } 273 | 274 | //---------------------------------------------------------------------------- 275 | // Listener interface for onActivityResult 276 | // 277 | 278 | public interface ActivityResultListener { 279 | void onActivityResult(int requestCode, int resultCode, Intent data); 280 | } 281 | 282 | private List activityResultListeners = null; 283 | 284 | public void registerActivityResultListener(ActivityResultListener listener) { 285 | if ( this.activityResultListeners == null ) 286 | this.activityResultListeners = Collections.synchronizedList(new ArrayList()); 287 | this.activityResultListeners.add(listener); 288 | } 289 | 290 | public void unregisterActivityResultListener(ActivityResultListener listener) { 291 | if ( this.activityResultListeners == null ) 292 | return; 293 | this.activityResultListeners.remove(listener); 294 | } 295 | 296 | @Override 297 | protected void onActivityResult(int requestCode, int resultCode, Intent intent) { 298 | if ( this.activityResultListeners == null ) 299 | return; 300 | this.onResume(); 301 | synchronized ( this.activityResultListeners ) { 302 | Iterator iterator = this.activityResultListeners.iterator(); 303 | while ( iterator.hasNext() ) 304 | (iterator.next()).onActivityResult(requestCode, resultCode, intent); 305 | } 306 | } 307 | 308 | public static void start_service( 309 | String serviceTitle, 310 | String serviceDescription, 311 | String pythonServiceArgument 312 | ) { 313 | _do_start_service( 314 | serviceTitle, serviceDescription, pythonServiceArgument, true 315 | ); 316 | } 317 | 318 | public static void start_service_not_as_foreground( 319 | String serviceTitle, 320 | String serviceDescription, 321 | String pythonServiceArgument 322 | ) { 323 | _do_start_service( 324 | serviceTitle, serviceDescription, pythonServiceArgument, false 325 | ); 326 | } 327 | 328 | public static void _do_start_service( 329 | String serviceTitle, 330 | String serviceDescription, 331 | String pythonServiceArgument, 332 | boolean showForegroundNotification 333 | ) { 334 | Intent serviceIntent = new Intent(PythonActivity.mActivity, PythonService.class); 335 | String argument = PythonActivity.mActivity.getFilesDir().getAbsolutePath(); 336 | String app_root_dir = PythonActivity.mActivity.getAppRoot(); 337 | String entry_point = PythonActivity.mActivity.getEntryPoint(app_root_dir + "/service"); 338 | serviceIntent.putExtra("androidPrivate", argument); 339 | serviceIntent.putExtra("androidArgument", app_root_dir); 340 | serviceIntent.putExtra("serviceEntrypoint", "service/" + entry_point); 341 | serviceIntent.putExtra("pythonName", "python"); 342 | serviceIntent.putExtra("pythonHome", app_root_dir); 343 | serviceIntent.putExtra("pythonPath", app_root_dir + ":" + app_root_dir + "/lib"); 344 | serviceIntent.putExtra("serviceStartAsForeground", 345 | (showForegroundNotification ? "true" : "false") 346 | ); 347 | serviceIntent.putExtra("serviceTitle", serviceTitle); 348 | serviceIntent.putExtra("serviceDescription", serviceDescription); 349 | serviceIntent.putExtra("pythonServiceArgument", pythonServiceArgument); 350 | PythonActivity.mActivity.startService(serviceIntent); 351 | } 352 | 353 | public static void stop_service() { 354 | Intent serviceIntent = new Intent(PythonActivity.mActivity, PythonService.class); 355 | PythonActivity.mActivity.stopService(serviceIntent); 356 | } 357 | 358 | /** Loading screen view **/ 359 | public static ImageView mImageView = null; 360 | /** Whether main routine/actual app has started yet **/ 361 | protected boolean mAppConfirmedActive = false; 362 | /** Timer for delayed loading screen removal. **/ 363 | protected Timer loadingScreenRemovalTimer = null; 364 | 365 | // Overridden since it's called often, to check whether to remove the 366 | // loading screen: 367 | @Override 368 | protected boolean sendCommand(int command, Object data) { 369 | boolean result = super.sendCommand(command, data); 370 | considerLoadingScreenRemoval(); 371 | return result; 372 | } 373 | 374 | /** Confirm that the app's main routine has been launched. 375 | **/ 376 | @Override 377 | public void appConfirmedActive() { 378 | if (!mAppConfirmedActive) { 379 | Log.v(TAG, "appConfirmedActive() -> preparing loading screen removal"); 380 | mAppConfirmedActive = true; 381 | considerLoadingScreenRemoval(); 382 | } 383 | } 384 | 385 | /** This is called from various places to check whether the app's main 386 | * routine has been launched already, and if it has, then the loading 387 | * screen will be removed. 388 | **/ 389 | public void considerLoadingScreenRemoval() { 390 | if (loadingScreenRemovalTimer != null) 391 | return; 392 | runOnUiThread(new Runnable() { 393 | public void run() { 394 | if (((PythonActivity)PythonActivity.mSingleton).mAppConfirmedActive && 395 | loadingScreenRemovalTimer == null) { 396 | // Remove loading screen but with a delay. 397 | // (app can use p4a's android.loadingscreen module to 398 | // do it quicker if it wants to) 399 | // get a handler (call from main thread) 400 | // this will run when timer elapses 401 | TimerTask removalTask = new TimerTask() { 402 | @Override 403 | public void run() { 404 | // post a runnable to the handler 405 | runOnUiThread(new Runnable() { 406 | @Override 407 | public void run() { 408 | PythonActivity activity = 409 | ((PythonActivity)PythonActivity.mSingleton); 410 | if (activity != null) 411 | activity.removeLoadingScreen(); 412 | } 413 | }); 414 | } 415 | }; 416 | loadingScreenRemovalTimer = new Timer(); 417 | loadingScreenRemovalTimer.schedule(removalTask, 5000); 418 | } 419 | } 420 | }); 421 | } 422 | 423 | public void removeLoadingScreen() { 424 | runOnUiThread(new Runnable() { 425 | public void run() { 426 | if (PythonActivity.mImageView != null && 427 | PythonActivity.mImageView.getParent() != null) { 428 | ((ViewGroup)PythonActivity.mImageView.getParent()).removeView( 429 | PythonActivity.mImageView); 430 | PythonActivity.mImageView = null; 431 | } 432 | } 433 | }); 434 | } 435 | 436 | public String getEntryPoint(String search_dir) { 437 | /* Get the main file (.pyc|.pyo|.py) depending on if we 438 | * have a compiled version or not. 439 | */ 440 | List entryPoints = new ArrayList(); 441 | entryPoints.add("main.pyo"); // python 2 compiled files 442 | entryPoints.add("main.pyc"); // python 3 compiled files 443 | for (String value : entryPoints) { 444 | File mainFile = new File(search_dir + "/" + value); 445 | if (mainFile.exists()) { 446 | return value; 447 | } 448 | } 449 | return "main.py"; 450 | } 451 | 452 | protected void showLoadingScreen() { 453 | // load the bitmap 454 | // 1. if the image is valid and we don't have layout yet, assign this bitmap 455 | // as main view. 456 | // 2. if we have a layout, just set it in the layout. 457 | // 3. If we have an mImageView already, then do nothing because it will have 458 | // already been made the content view or added to the layout. 459 | 460 | if (mImageView == null) { 461 | int presplashId = this.resourceManager.getIdentifier("presplash", "drawable"); 462 | InputStream is = this.getResources().openRawResource(presplashId); 463 | Bitmap bitmap = null; 464 | try { 465 | bitmap = BitmapFactory.decodeStream(is); 466 | } finally { 467 | try { 468 | is.close(); 469 | } catch (IOException e) {}; 470 | } 471 | 472 | mImageView = new ImageView(this); 473 | mImageView.setImageBitmap(bitmap); 474 | 475 | /* 476 | * Set the presplash loading screen background color 477 | * https://developer.android.com/reference/android/graphics/Color.html 478 | * Parse the color string, and return the corresponding color-int. 479 | * If the string cannot be parsed, throws an IllegalArgumentException exception. 480 | * Supported formats are: #RRGGBB #AARRGGBB or one of the following names: 481 | * 'red', 'blue', 'green', 'black', 'white', 'gray', 'cyan', 'magenta', 'yellow', 482 | * 'lightgray', 'darkgray', 'grey', 'lightgrey', 'darkgrey', 'aqua', 'fuchsia', 483 | * 'lime', 'maroon', 'navy', 'olive', 'purple', 'silver', 'teal'. 484 | */ 485 | String backgroundColor = resourceManager.getString("presplash_color"); 486 | if (backgroundColor != null) { 487 | try { 488 | mImageView.setBackgroundColor(Color.parseColor(backgroundColor)); 489 | } catch (IllegalArgumentException e) {} 490 | } 491 | mImageView.setLayoutParams(new ViewGroup.LayoutParams( 492 | ViewGroup.LayoutParams.FILL_PARENT, 493 | ViewGroup.LayoutParams.FILL_PARENT)); 494 | mImageView.setScaleType(ImageView.ScaleType.FIT_CENTER); 495 | } 496 | 497 | try { 498 | if (mLayout == null) { 499 | setContentView(mImageView); 500 | } else if (PythonActivity.mImageView.getParent() == null) { 501 | mLayout.addView(mImageView); 502 | } 503 | } catch (IllegalStateException e) { 504 | // The loading screen can be attempted to be applied twice if app 505 | // is tabbed in/out, quickly. 506 | // (Gives error "The specified child already has a parent. 507 | // You must call removeView() on the child's parent first.") 508 | } 509 | } 510 | 511 | @Override 512 | protected void onPause() { 513 | if (this.mWakeLock != null && mWakeLock.isHeld()) { 514 | this.mWakeLock.release(); 515 | } 516 | 517 | Log.v(TAG, "onPause()"); 518 | try { 519 | super.onPause(); 520 | } catch (UnsatisfiedLinkError e) { 521 | // Catch pause while still in loading screen failing to 522 | // call native function (since it's not yet loaded) 523 | } 524 | } 525 | 526 | @Override 527 | protected void onResume() { 528 | if (this.mWakeLock != null) { 529 | this.mWakeLock.acquire(); 530 | } 531 | Log.v(TAG, "onResume()"); 532 | try { 533 | super.onResume(); 534 | } catch (UnsatisfiedLinkError e) { 535 | // Catch resume while still in loading screen failing to 536 | // call native function (since it's not yet loaded) 537 | } 538 | considerLoadingScreenRemoval(); 539 | } 540 | 541 | @Override 542 | public void onWindowFocusChanged(boolean hasFocus) { 543 | try { 544 | super.onWindowFocusChanged(hasFocus); 545 | } catch (UnsatisfiedLinkError e) { 546 | // Catch window focus while still in loading screen failing to 547 | // call native function (since it's not yet loaded) 548 | } 549 | considerLoadingScreenRemoval(); 550 | } 551 | 552 | /** 553 | * Used by android.permissions p4a module to register a call back after 554 | * requesting runtime permissions 555 | **/ 556 | public interface PermissionsCallback { 557 | void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults); 558 | } 559 | 560 | private PermissionsCallback permissionCallback; 561 | private boolean havePermissionsCallback = false; 562 | 563 | public void addPermissionsCallback(PermissionsCallback callback) { 564 | permissionCallback = callback; 565 | havePermissionsCallback = true; 566 | Log.v(TAG, "addPermissionsCallback(): Added callback for onRequestPermissionsResult"); 567 | } 568 | 569 | @Override 570 | public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { 571 | Log.v(TAG, "onRequestPermissionsResult()"); 572 | if (havePermissionsCallback) { 573 | Log.v(TAG, "onRequestPermissionsResult passed to callback"); 574 | permissionCallback.onRequestPermissionsResult(requestCode, permissions, grantResults); 575 | } 576 | super.onRequestPermissionsResult(requestCode, permissions, grantResults); 577 | } 578 | 579 | /** 580 | * Used by android.permissions p4a module to check a permission 581 | **/ 582 | public boolean checkCurrentPermission(String permission) { 583 | if (android.os.Build.VERSION.SDK_INT < 23) 584 | return true; 585 | 586 | try { 587 | java.lang.reflect.Method methodCheckPermission = 588 | Activity.class.getMethod("checkSelfPermission", String.class); 589 | Object resultObj = methodCheckPermission.invoke(this, permission); 590 | int result = Integer.parseInt(resultObj.toString()); 591 | if (result == PackageManager.PERMISSION_GRANTED) 592 | return true; 593 | } catch (IllegalAccessException | NoSuchMethodException | 594 | InvocationTargetException e) { 595 | } 596 | return false; 597 | } 598 | 599 | /** 600 | * Used by android.permissions p4a module to request runtime permissions 601 | **/ 602 | public void requestPermissionsWithRequestCode(String[] permissions, int requestCode) { 603 | if (android.os.Build.VERSION.SDK_INT < 23) 604 | return; 605 | try { 606 | java.lang.reflect.Method methodRequestPermission = 607 | Activity.class.getMethod("requestPermissions", 608 | String[].class, int.class); 609 | methodRequestPermission.invoke(this, permissions, requestCode); 610 | } catch (IllegalAccessException | NoSuchMethodException | 611 | InvocationTargetException e) { 612 | } 613 | } 614 | 615 | public void requestPermissions(String[] permissions) { 616 | requestPermissionsWithRequestCode(permissions, 1); 617 | } 618 | } 619 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import time 2 | from jnius import autoclass, cast 3 | from kivy.app import App 4 | from kivy.lang import Builder 5 | from kivy.utils import platform 6 | from kivy.logger import Logger 7 | 8 | 9 | if platform == 'android': 10 | Intent = autoclass('android.content.Intent') 11 | KivyAlarmReceiver = autoclass('org.kivy.android.KivyAlarmReceiver') 12 | PythonActivity = autoclass('org.kivy.android.PythonActivity') 13 | mActivity = PythonActivity.mActivity 14 | Intent = autoclass('android.content.Intent') 15 | AlarmManager = autoclass('android.app.AlarmManager') 16 | PendingIntent = autoclass('android.app.PendingIntent') 17 | Context = autoclass('android.content.Context') 18 | 19 | 20 | KV = """ 21 | FloatLayout: 22 | TextInput: 23 | id: t 24 | pos_hint: {'center': (.5, .65)} 25 | size_hint: .3, .05 26 | filter: int 27 | Label: 28 | pos_hint: {'center': (.5, .6)} 29 | text: 'after how many minutes should be fired' 30 | Button: 31 | pos_hint: {'center': (.3, .45)} 32 | size_hint: .3, .075 33 | text: 'schedule alarm' 34 | on_release: app.start_alarm(t.text) 35 | Button: 36 | text: 'stop service' 37 | pos_hint: {'center': (.7, .45)} 38 | size_hint: .3, .075 39 | on_release: app.stop_service() 40 | Label: 41 | id: lbl 42 | pos_hint: {'center': (.5, .35)} 43 | text: 'kivy alarm manger with service' 44 | """ 45 | 46 | 47 | class Application(App): 48 | 49 | def build(self): 50 | return Builder.load_string(KV) 51 | 52 | def start_alarm(self, min): 53 | try: 54 | min = int(min) 55 | except Exception: 56 | Logger.info('Not a valid number: Minutes set to 1') 57 | min = 1 58 | context = mActivity.getApplicationContext() 59 | alarmSetTime = int(round(time.time() * 1000)) + 1000 * 60 * int(min) 60 | alarmIntent = Intent() 61 | alarmIntent.setClass(context, KivyAlarmReceiver) 62 | alarmIntent.setAction("org.kivy.android.ACTION_START_ALARM") 63 | pendingIntent = PendingIntent.getBroadcast( 64 | context, 18, alarmIntent, PendingIntent.FLAG_UPDATE_CURRENT) 65 | alarm = cast( 66 | AlarmManager, context.getSystemService(Context.ALARM_SERVICE)) 67 | alarm.setExactAndAllowWhileIdle( 68 | AlarmManager.RTC_WAKEUP, alarmSetTime, pendingIntent) 69 | 70 | def stop_service(self): 71 | mActivity.stop_service() 72 | 73 | 74 | if __name__ == "__main__": 75 | app = Application() 76 | app.run() 77 | -------------------------------------------------------------------------------- /service/main.py: -------------------------------------------------------------------------------- 1 | from time import localtime, asctime, sleep, time 2 | from jnius import cast, autoclass 3 | 4 | 5 | Intent = autoclass('android.content.Intent') 6 | KivyAlarmReceiver = autoclass('org.kivy.android.KivyAlarmReceiver') 7 | mService = autoclass('org.kivy.android.PythonService').mService 8 | Intent = autoclass('android.content.Intent') 9 | AlarmManager = autoclass('android.app.AlarmManager') 10 | PendingIntent = autoclass('android.app.PendingIntent') 11 | Context = autoclass('android.content.Context') 12 | 13 | 14 | class KivyService: 15 | def __init__(self): 16 | pass 17 | 18 | def reschedule_alarm(self, minutes): 19 | context = mService.getApplicationContext() 20 | alarmSetTime = int(round(time() * 1000)) + 1000 * 60 * minutes 21 | alarmIntent = Intent() 22 | alarmIntent.setClass(context, KivyAlarmReceiver) 23 | alarmIntent.setAction("org.kivy.android.ACTION_START_ALARM") 24 | pendingIntent = PendingIntent.getBroadcast( 25 | context, 18, alarmIntent, PendingIntent.FLAG_UPDATE_CURRENT) 26 | alarm = cast( 27 | AlarmManager, context.getSystemService(Context.ALARM_SERVICE)) 28 | alarm.setExactAndAllowWhileIdle( 29 | AlarmManager.RTC_WAKEUP, alarmSetTime, pendingIntent) 30 | 31 | def start(self): 32 | i = 0 33 | while True: 34 | sleep(2) 35 | i += 1 36 | print(asctime(localtime()).encode('utf8')) 37 | if i >= 30: 38 | self.reschedule_alarm(1) 39 | break 40 | 41 | 42 | if __name__ == '__main__': 43 | service = KivyService() 44 | service.start() 45 | --------------------------------------------------------------------------------