diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..07c1b1d Binary files /dev/null and b/.DS_Store differ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3e6052b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,87 @@ +## ChangeLog +#### Version 0.7.2 (02.02.2017) +- Fixed app freeze on iOS using wkwebview-engine +- Websocket sample in SampleApp + +#### Version 0.7.1 (30.01.2017) +- Bug fixes for iOS9 and Android +- Allow app to be excluded from recent list on Android + +#### Version 0.7.0 (27.01.2017) +- __Features__ + - Support for tAmazon FireOS + - Support for the browser platform + - Ability to configure icon and color on Android + - Allow app to move to foreground on Android + - Allow app to move to background on Android + - Allow app to override back button behaviour on Android + - New events for when the mode has been enabled/disabled +- __Improvements__ + - Various enhancements and bug fixes for all platforms + - Support for latest platform and OS versions + - Multi line text on Android + - Multiple listeners for same event + - Compatibility with cordova-plugin-geolocation + - Compatibility with cordova-plugin-crosswalk-webview + - Compatibility with cordova-plugin-wkwebview-engine + - New sample app +- __Fixes__ + - Silent mode issues on Android + - Lock screen issues on Android + - Callback not called on Android + - Notification shows app info with cordova-android@6 + - Other apps audio interruption on iOS +- __Changes__ + - Deprecate event callbacks + - Notification not visible by default on lock screen + - Remove ticker property on Android + - Remove unexpected back button handler + - Remove support for wp8 platform + +#### Version 0.6.5 (29.02.2016) +- Published on npm +- Updated dependency ID for the device plug-in + +#### Version 0.6.4 (03.03.2015) +- Resolve possibly dependency conflict + +#### Version 0.6.3 (01.01.2015) +- [feature:] Silent mode for Android + +#### Version 0.6.2 (14.12.2014) +- [bugfix:] Type error +- [bugfix:] Wrong default values for `isEnabled` and `isActive`. + +#### Version 0.6.1 (14.12.2014) +- [enhancement:] Set default settings through `setDefaults`. +- [enhancement:] New method `isEnabled` to receive if mode is enabled. +- [enhancement:] New method `isActive` to receive if mode is active. +- [bugfix:] Events caused thread collision. + + +#### Version 0.6.0 (14.12.2014) +- [feature:] Android support +- [feature:] Change Android notification through `configure`. +- [feature:] `onactivate`, `ondeactivate` and `onfailure` callbacks. +- [___change___:] Disabled by default +- [enhancement:] Get default settings through `getDefaults`. +- [enhancement:] iOS does not require user permissions, internet connection and geo location anymore. + +#### Version 0.5.0 (13.02.2014) +- __retired__ + +#### Version 0.4.1 (13.02.2014) +- Release under the Apache 2.0 license. +- [enhancement:] Location tracking is only activated on WP8 if the location service is available. +- [bigfix:] Nullpointer exception on WP8. + +#### Version 0.4.0 (10.10.2013) +- Added WP8 support
+ The plugin turns the app into an location tracking app *(for the time it runs in the background)*. + +#### Version 0.2.1 (09.10.2013) +- Added js interface to manually enable/disable the background mode. + +#### Version 0.2.0 (08.10.2013) +- Added iOS (>= 5) support
+ The plugin turns the app into an location tracking app for the time it runs in the background. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9999e87 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2013 appPlant GmbH + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..78e9329 --- /dev/null +++ b/package.json @@ -0,0 +1,59 @@ +{ + "_from": "cordova-plugin-background-mode", + "_id": "cordova-plugin-background-mode@0.7.2", + "_inBundle": false, + "_integrity": "sha1-6vVjnAZahyMbPs91em4osQHQMy8=", + "_location": "/cordova-plugin-background-mode", + "_phantomChildren": {}, + "_requested": { + "type": "tag", + "registry": true, + "raw": "cordova-plugin-background-mode", + "name": "cordova-plugin-background-mode", + "escapedName": "cordova-plugin-background-mode", + "rawSpec": "", + "saveSpec": null, + "fetchSpec": "latest" + }, + "_requiredBy": [ + "#USER", + "/" + ], + "_resolved": "https://registry.npm.taobao.org/cordova-plugin-background-mode/download/cordova-plugin-background-mode-0.7.2.tgz", + "_shasum": "eaf5639c065a87231b3ecf757a6e28b101d0332f", + "_spec": "cordova-plugin-background-mode", + "_where": "/Users/zhangxiongwang/Documents/workspace/laoting", + "author": { + "name": "Sebastián Katzer" + }, + "bugs": { + "url": "https://github.com/katzer/cordova-plugin-background-mode/issues" + }, + "bundleDependencies": false, + "deprecated": false, + "description": "Prevent app from going to sleep in background.", + "engines": [ + { + "name": "cordova", + "version": ">=6.0.0" + } + ], + "homepage": "https://github.com/katzer/cordova-plugin-background-mode#readme", + "keywords": [ + "appplant", + "background", + "cordova", + "ecosystem:cordova" + ], + "license": "Apache 2.0", + "name": "cordova-plugin-background-mode", + "platforms": [ + "ios", + "android" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/katzer/cordova-plugin-background-mode.git" + }, + "version": "0.7.2" +} diff --git a/plugin.xml b/plugin.xml new file mode 100644 index 0000000..4121a33 --- /dev/null +++ b/plugin.xml @@ -0,0 +1,132 @@ + + + + + BackgroundMode + + Prevent apps from going to sleep in background. + + https://github.com/katzer/cordova-plugin-background-mode.git + + appplant, background + + Apache 2.0 + + Sebastián Katzer + + + + + + + + + + + + + + + + + + + + + + + + + + + + audio + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/android/BackgroundExt.java b/src/android/BackgroundExt.java new file mode 100644 index 0000000..69e4ec6 --- /dev/null +++ b/src/android/BackgroundExt.java @@ -0,0 +1,178 @@ +/* + Copyright 2013-2017 appPlant GmbH + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +package de.appplant.cordova.plugin.background; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.app.ActivityManager; +import android.app.ActivityManager.AppTask; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.view.View; + +import org.apache.cordova.CordovaInterface; +import org.apache.cordova.CordovaWebView; + +import java.lang.ref.WeakReference; +import java.lang.reflect.Method; +import java.util.List; + +class BackgroundExt { + + // Weak reference to the cordova interface passed by the plugin + private final WeakReference cordova; + + // Weak reference to the cordova web view passed by the plugin + private final WeakReference webView; + + /** + * Initialize the extension to perform non-background related tasks. + * + * @param cordova The cordova interface. + * @param webView The cordova web view. + */ + private BackgroundExt(CordovaInterface cordova, CordovaWebView webView) { + this.cordova = new WeakReference(cordova); + this.webView = new WeakReference(webView); + } + + /** + * Executes the request. + * + * @param action The action to execute. + * @param cordova The cordova interface. + * @param webView The cordova web view. + */ + static void execute(String action, CordovaInterface cordova, + CordovaWebView webView) { + + BackgroundExt ext = new BackgroundExt(cordova, webView); + + if (action.equalsIgnoreCase("optimizations")) { + ext.disableWebViewOptimizations(); + } + + if (action.equalsIgnoreCase("background")) { + ext.moveToBackground(); + } + + if (action.equalsIgnoreCase("foreground")) { + ext.moveToForeground(); + } + + if (action.equalsIgnoreCase("tasklist")) { + ext.excludeFromTaskList(); + } + } + + /** + * Move app to background. + */ + private void moveToBackground() { + Intent intent = new Intent(Intent.ACTION_MAIN); + + intent.addCategory(Intent.CATEGORY_HOME); + getActivity().startActivity(intent); + } + + /** + * Move app to foreground. + */ + private void moveToForeground() { + Activity app = getActivity(); + String pkgName = app.getPackageName(); + + Intent intent = app + .getPackageManager() + .getLaunchIntentForPackage(pkgName); + + intent.addFlags( Intent.FLAG_ACTIVITY_REORDER_TO_FRONT + | Intent.FLAG_ACTIVITY_SINGLE_TOP); + + app.startActivity(intent); + } + + /** + * Enable GPS position tracking while in background. + */ + private void disableWebViewOptimizations() { + Thread thread = new Thread(){ + public void run() { + try { + Thread.sleep(1000); + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + View view = webView.get().getEngine().getView(); + + try { + Class xWalkCls = Class.forName( + "org.crosswalk.engine.XWalkCordovaView"); + + Method onShowMethod = + xWalkCls.getMethod("onShow"); + + onShowMethod.invoke(view); + } catch (Exception e){ + view.dispatchWindowVisibilityChanged(View.VISIBLE); + } + } + }); + } catch (InterruptedException e) { + // do nothing + } + } + }; + + thread.start(); + } + + /** + * Exclude the app from the recent tasks list. + */ + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private void excludeFromTaskList() { + ActivityManager am = (ActivityManager) getActivity() + .getSystemService(Context.ACTIVITY_SERVICE); + + if (am == null || Build.VERSION.SDK_INT < 21) + return; + + List tasks = am.getAppTasks(); + + if (tasks == null || tasks.isEmpty()) + return; + + tasks.get(0).setExcludeFromRecents(true); + } + + /** + * The activity referenced by cordova. + * + * @return The main activity of the app. + */ + Activity getActivity() { + return cordova.get().getActivity(); + } + +} \ No newline at end of file diff --git a/src/android/BackgroundMode.java b/src/android/BackgroundMode.java new file mode 100644 index 0000000..aebb3ca --- /dev/null +++ b/src/android/BackgroundMode.java @@ -0,0 +1,301 @@ +/* + Copyright 2013-2017 appPlant GmbH + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +package de.appplant.cordova.plugin.background; + +import android.app.Activity; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.IBinder; + +import org.apache.cordova.CallbackContext; +import org.apache.cordova.CordovaPlugin; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +public class BackgroundMode extends CordovaPlugin { + + // Event types for callbacks + private enum Event { + ACTIVATE, DEACTIVATE, FAILURE + } + + // Plugin namespace + private static final String JS_NAMESPACE = + "cordova.plugins.backgroundMode"; + + // Flag indicates if the app is in background or foreground + private boolean inBackground = false; + + // Flag indicates if the plugin is enabled or disabled + private boolean isDisabled = true; + + // Flag indicates if the service is bind + private boolean isBind = false; + + // Default settings for the notification + private static JSONObject defaultSettings = new JSONObject(); + + // Service that keeps the app awake + private ForegroundService service; + + // Used to (un)bind the service to with the activity + private final ServiceConnection connection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + ForegroundService.ForegroundBinder binder = + (ForegroundService.ForegroundBinder) service; + + BackgroundMode.this.service = binder.getService(); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + fireEvent(Event.FAILURE, "'service disconnected'"); + } + }; + + /** + * Executes the request. + * + * @param action The action to execute. + * @param args The exec() arguments. + * @param callback The callback context used when + * calling back into JavaScript. + * + * @return Returning false results in a "MethodNotFound" error. + * + * @throws JSONException + */ + @Override + public boolean execute (String action, JSONArray args, + CallbackContext callback) throws JSONException { + + if (action.equalsIgnoreCase("configure")) { + JSONObject settings = args.getJSONObject(0); + boolean update = args.getBoolean(1); + + configure(settings, update); + } + else + if (action.equalsIgnoreCase("enable")) { + enableMode(); + } + else + if (action.equalsIgnoreCase("disable")) { + disableMode(); + } + else { + BackgroundExt.execute(action, cordova, webView); + } + + callback.success(); + + return true; + } + + /** + * Called when the system is about to start resuming a previous activity. + * + * @param multitasking Flag indicating if multitasking is turned on for app. + */ + @Override + public void onPause(boolean multitasking) { + super.onPause(multitasking); + inBackground = true; + startService(); + } + + /** + * Called when the activity will start interacting with the user. + * + * @param multitasking Flag indicating if multitasking is turned on for app. + */ + @Override + public void onResume(boolean multitasking) { + super.onResume(multitasking); + inBackground = false; + stopService(); + } + + /** + * Called when the activity will be destroyed. + */ + @Override + public void onDestroy() { + super.onDestroy(); + stopService(); + } + + /** + * Enable the background mode. + */ + private void enableMode() { + isDisabled = false; + + if (inBackground) { + startService(); + } + } + + /** + * Disable the background mode. + */ + private void disableMode() { + stopService(); + isDisabled = true; + } + + /** + * Update the default settings and configure the notification. + * + * @param settings The settings + * @param update A truthy value means to update the running service. + */ + private void configure(JSONObject settings, boolean update) { + if (update) { + updateNotification(settings); + } else { + setDefaultSettings(settings); + } + } + + /** + * Update the default settings for the notification. + * + * @param settings The new default settings + */ + private void setDefaultSettings(JSONObject settings) { + defaultSettings = settings; + } + + /** + * The settings for the new/updated notification. + * + * @return + * updateSettings if set or default settings + */ + protected static JSONObject getSettings() { + return defaultSettings; + } + + /** + * Update the notification. + * + * @param settings The config settings + */ + private void updateNotification(JSONObject settings) { + if (isBind) { + service.updateNotification(settings); + } + } + + /** + * Bind the activity to a background service and put them into foreground + * state. + */ + private void startService() { + Activity context = cordova.getActivity(); + + if (isDisabled || isBind) + return; + + Intent intent = new Intent( + context, ForegroundService.class); + + try { + context.bindService(intent, + connection, Context.BIND_AUTO_CREATE); + + fireEvent(Event.ACTIVATE, null); + + context.startService(intent); + } catch (Exception e) { + fireEvent(Event.FAILURE, String.format("'%s'", e.getMessage())); + } + + isBind = true; + } + + /** + * Bind the activity to a background service and put them into foreground + * state. + */ + private void stopService() { + Activity context = cordova.getActivity(); + + Intent intent = new Intent( + context, ForegroundService.class); + + if (!isBind) + return; + + fireEvent(Event.DEACTIVATE, null); + + context.unbindService(connection); + context.stopService(intent); + + isBind = false; + } + + /** + * Fire vent with some parameters inside the web view. + * + * @param event The name of the event + * @param params Optional arguments for the event + */ + private void fireEvent (Event event, String params) { + String eventName; + + switch (event) { + case ACTIVATE: + eventName = "activate"; break; + case DEACTIVATE: + eventName = "deactivate"; break; + default: + eventName = "failure"; + } + + String active = event == Event.ACTIVATE ? "true" : "false"; + + String flag = String.format("%s._isActive=%s;", + JS_NAMESPACE, active); + + String depFn = String.format("%s.on%s(%s);", + JS_NAMESPACE, eventName, params); + + String fn = String.format("%s.fireEvent('%s',%s);", + JS_NAMESPACE, eventName, params); + + final String js = flag + fn + depFn; + + cordova.getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + webView.loadUrl("javascript:" + js); + } + }); + } + +} diff --git a/src/android/ForegroundService.java b/src/android/ForegroundService.java new file mode 100644 index 0000000..806c956 --- /dev/null +++ b/src/android/ForegroundService.java @@ -0,0 +1,323 @@ +/* + Copyright 2013 Sebastián Katzer + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +package de.appplant.cordova.plugin.background; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.os.Binder; +import android.os.Build; +import android.os.IBinder; +import android.os.PowerManager; +import android.app.NotificationChannel; + +import org.json.JSONObject; + +import static android.os.PowerManager.PARTIAL_WAKE_LOCK; + +/** + * Puts the service in a foreground state, where the system considers it to be + * something the user is actively aware of and thus not a candidate for killing + * when low on memory. + */ +public class ForegroundService extends Service { + + // Fixed ID for the 'foreground' notification + public static final int NOTIFICATION_ID = -574543954; + + // Default title of the background notification + private static final String NOTIFICATION_TITLE = + "App is running in background"; + + // Default text of the background notification + private static final String NOTIFICATION_TEXT = + "Doing heavy tasks."; + + // Default icon of the background notification + private static final String NOTIFICATION_ICON = "icon"; + + // Binder given to clients + private final IBinder binder = new ForegroundBinder(); + + // Partial wake lock to prevent the app from going to sleep when locked + private PowerManager.WakeLock wakeLock; + + /** + * Allow clients to call on to the service. + */ + @Override + public IBinder onBind (Intent intent) { + return binder; + } + + /** + * Class used for the client Binder. Because we know this service always + * runs in the same process as its clients, we don't need to deal with IPC. + */ + class ForegroundBinder extends Binder + { + ForegroundService getService() + { + // Return this instance of ForegroundService + // so clients can call public methods + return ForegroundService.this; + } + } + + /** + * Put the service in a foreground state to prevent app from being killed + * by the OS. + */ + @Override + public void onCreate() + { + super.onCreate(); + keepAwake(); + } + + /** + * No need to run headless on destroy. + */ + @Override + public void onDestroy() + { + super.onDestroy(); + sleepWell(); + } + + /** + * Prevent Android from stopping the background service automatically. + */ + @Override + public int onStartCommand (Intent intent, int flags, int startId) { + return START_STICKY; + } + + /** + * Put the service in a foreground state to prevent app from being killed + * by the OS. + */ + @SuppressLint("WakelockTimeout") + private void keepAwake() + { + JSONObject settings = BackgroundMode.getSettings(); + boolean isSilent = settings.optBoolean("silent", false); + + if (!isSilent) { + startForeground(NOTIFICATION_ID, makeNotification()); + } + + PowerManager pm = (PowerManager)getSystemService(POWER_SERVICE); + + wakeLock = pm.newWakeLock( + PARTIAL_WAKE_LOCK, "backgroundmode:wakelock"); + + wakeLock.acquire(); + } + + /** + * Stop background mode. + */ + private void sleepWell() + { + stopForeground(true); + getNotificationManager().cancel(NOTIFICATION_ID); + + if (wakeLock != null) { + wakeLock.release(); + wakeLock = null; + } + } + + /** + * Create a notification as the visible part to be able to put the service + * in a foreground state by using the default settings. + */ + private Notification makeNotification() + { + return makeNotification(BackgroundMode.getSettings()); + } + + /** + * Create a notification as the visible part to be able to put the service + * in a foreground state. + * + * @param settings The config settings + */ + private Notification makeNotification (JSONObject settings) + { + // use channelid for Oreo and higher + String CHANNEL_ID = "cordova-plugin-background-mode-id"; + if(Build.VERSION.SDK_INT >= 26){ + // The user-visible name of the channel. + CharSequence name = "cordova-plugin-background-mode"; + // The user-visible description of the channel. + String description = "cordova-plugin-background-moden notification"; + + int importance = NotificationManager.IMPORTANCE_LOW; + + NotificationChannel mChannel = new NotificationChannel(CHANNEL_ID, name,importance); + + // Configure the notification channel. + mChannel.setDescription(description); + + getNotificationManager().createNotificationChannel(mChannel); + } + String title = settings.optString("title", NOTIFICATION_TITLE); + String text = settings.optString("text", NOTIFICATION_TEXT); + boolean bigText = settings.optBoolean("bigText", false); + + Context context = getApplicationContext(); + String pkgName = context.getPackageName(); + Intent intent = context.getPackageManager() + .getLaunchIntentForPackage(pkgName); + + Notification.Builder notification = new Notification.Builder(context) + .setContentTitle(title) + .setContentText(text) + .setOngoing(true) + .setSmallIcon(getIconResId(settings)); + + if(Build.VERSION.SDK_INT >= 26){ + notification.setChannelId(CHANNEL_ID); + } + + if (settings.optBoolean("hidden", true)) { + notification.setPriority(Notification.PRIORITY_MIN); + } + + if (bigText || text.contains("\n")) { + notification.setStyle( + new Notification.BigTextStyle().bigText(text)); + } + + setColor(notification, settings); + + if (intent != null && settings.optBoolean("resume")) { + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); + PendingIntent contentIntent = PendingIntent.getActivity( + context, NOTIFICATION_ID, intent, + PendingIntent.FLAG_UPDATE_CURRENT); + + + notification.setContentIntent(contentIntent); + } + + return notification.build(); + } + + /** + * Update the notification. + * + * @param settings The config settings + */ + protected void updateNotification (JSONObject settings) + { + boolean isSilent = settings.optBoolean("silent", false); + + if (isSilent) { + stopForeground(true); + return; + } + + Notification notification = makeNotification(settings); + getNotificationManager().notify(NOTIFICATION_ID, notification); + + } + + /** + * Retrieves the resource ID of the app icon. + * + * @param settings A JSON dict containing the icon name. + */ + private int getIconResId (JSONObject settings) + { + String icon = settings.optString("icon", NOTIFICATION_ICON); + + int resId = getIconResId(icon, "mipmap"); + + if (resId == 0) { + resId = getIconResId(icon, "drawable"); + } + + return resId; + } + + /** + * Retrieve resource id of the specified icon. + * + * @param icon The name of the icon. + * @param type The resource type where to look for. + * + * @return The resource id or 0 if not found. + */ + private int getIconResId (String icon, String type) + { + Resources res = getResources(); + String pkgName = getPackageName(); + + int resId = res.getIdentifier(icon, type, pkgName); + + if (resId == 0) { + resId = res.getIdentifier("icon", type, pkgName); + } + + return resId; + } + + /** + * Set notification color if its supported by the SDK. + * + * @param notification A Notification.Builder instance + * @param settings A JSON dict containing the color definition (red: FF0000) + */ + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private void setColor (Notification.Builder notification, JSONObject settings) + { + + String hex = settings.optString("color", null); + + if (Build.VERSION.SDK_INT < 21 || hex == null) + return; + + try { + int aRGB = Integer.parseInt(hex, 16) + 0xFF000000; + notification.setColor(aRGB); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Returns the shared notification service manager. + */ + private NotificationManager getNotificationManager() + { + return (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + } +} diff --git a/src/browser/BackgroundModeProxy.js b/src/browser/BackgroundModeProxy.js new file mode 100644 index 0000000..5ba7880 --- /dev/null +++ b/src/browser/BackgroundModeProxy.js @@ -0,0 +1,49 @@ +/* + Copyright 2013-2017 appPlant GmbH + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +/** + * Activates the background mode. When activated the application + * will be prevented from going to sleep while in background + * for the next time. + * + * @param [ Function ] success The success callback to use. + * @param [ Function ] error The error callback to use. + * + * @return [ Void ] + */ +exports.enable = function (success, error) { + success(); +}; + +/** + * Deactivates the background mode. When deactivated the application + * will not stay awake while in background. + * + * @param [ Function ] success The success callback to use. + * @param [ Function ] error The error callback to use. + * + * @return [ Void ] + */ +exports.disable = function (success, error) { + success(); +}; + +cordova.commandProxy.add('BackgroundMode', exports); diff --git a/src/ios/APPBackgroundMode.h b/src/ios/APPBackgroundMode.h new file mode 100644 index 0000000..e7ed1e5 --- /dev/null +++ b/src/ios/APPBackgroundMode.h @@ -0,0 +1,35 @@ +/* + Copyright 2013-2017 appPlant GmbH + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ + +#import +#import + +@interface APPBackgroundMode : CDVPlugin { + AVAudioPlayer* audioPlayer; + BOOL enabled; +} + +// Activate the background mode +- (void) enable:(CDVInvokedUrlCommand*)command; +// Deactivate the background mode +- (void) disable:(CDVInvokedUrlCommand*)command; + +@end diff --git a/src/ios/APPBackgroundMode.m b/src/ios/APPBackgroundMode.m new file mode 100644 index 0000000..0e05666 --- /dev/null +++ b/src/ios/APPBackgroundMode.m @@ -0,0 +1,277 @@ +/* + Copyright 2013-2017 appPlant GmbH + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ + +#import "APPMethodMagic.h" +#import "APPBackgroundMode.h" +#import + +@implementation APPBackgroundMode + +#pragma mark - +#pragma mark Constants + +NSString* const kAPPBackgroundJsNamespace = @"cordova.plugins.backgroundMode"; +NSString* const kAPPBackgroundEventActivate = @"activate"; +NSString* const kAPPBackgroundEventDeactivate = @"deactivate"; + + +#pragma mark - +#pragma mark Life Cycle + +/** + * Called by runtime once the Class has been loaded. + * Exchange method implementations to hook into their execution. + */ ++ (void) load +{ + [self swizzleWKWebViewEngine]; +} + +/** + * Initialize the plugin. + */ +- (void) pluginInitialize +{ + enabled = NO; + [self configureAudioPlayer]; + [self configureAudioSession]; + [self observeLifeCycle]; +} + +/** + * Register the listener for pause and resume events. + */ +- (void) observeLifeCycle +{ + NSNotificationCenter* listener = [NSNotificationCenter + defaultCenter]; + + [listener addObserver:self + selector:@selector(keepAwake) + name:UIApplicationDidEnterBackgroundNotification + object:nil]; + + [listener addObserver:self + selector:@selector(stopKeepingAwake) + name:UIApplicationWillEnterForegroundNotification + object:nil]; + + [listener addObserver:self + selector:@selector(handleAudioSessionInterruption:) + name:AVAudioSessionInterruptionNotification + object:nil]; +} + +#pragma mark - +#pragma mark Interface + +/** + * Enable the mode to stay awake + * when switching to background for the next time. + */ +- (void) enable:(CDVInvokedUrlCommand*)command +{ + if (enabled) + return; + + enabled = YES; + [self execCallback:command]; +} + +/** + * Disable the background mode + * and stop being active in background. + */ +- (void) disable:(CDVInvokedUrlCommand*)command +{ + if (!enabled) + return; + + enabled = NO; + [self stopKeepingAwake]; + [self execCallback:command]; +} + +#pragma mark - +#pragma mark Core + +/** + * Keep the app awake. + */ +- (void) keepAwake +{ + if (!enabled) + return; + + [audioPlayer play]; + [self fireEvent:kAPPBackgroundEventActivate]; +} + +/** + * Let the app going to sleep. + */ +- (void) stopKeepingAwake +{ + if (TARGET_IPHONE_SIMULATOR) { + NSLog(@"BackgroundMode: On simulator apps never pause in background!"); + } + + if (audioPlayer.isPlaying) { + [self fireEvent:kAPPBackgroundEventDeactivate]; + } + + [audioPlayer pause]; +} + +/** + * Configure the audio player. + */ +- (void) configureAudioPlayer +{ + NSString* path = [[NSBundle mainBundle] + pathForResource:@"appbeep" ofType:@"wav"]; + + NSURL* url = [NSURL fileURLWithPath:path]; + + + audioPlayer = [[AVAudioPlayer alloc] + initWithContentsOfURL:url error:NULL]; + + audioPlayer.volume = 0; + audioPlayer.numberOfLoops = -1; +}; + +/** + * Configure the audio session. + */ +- (void) configureAudioSession +{ + AVAudioSession* session = [AVAudioSession + sharedInstance]; + + // Don't activate the audio session yet + [session setActive:NO error:NULL]; + + // Play music even in background and dont stop playing music + // even another app starts playing sound + [session setCategory:AVAudioSessionCategoryPlayback + withOptions:AVAudioSessionCategoryOptionMixWithOthers + error:NULL]; + + // Active the audio session + [session setActive:YES error:NULL]; +}; + +#pragma mark - +#pragma mark Helper + +/** + * Simply invokes the callback without any parameter. + */ +- (void) execCallback:(CDVInvokedUrlCommand*)command +{ + CDVPluginResult *result = [CDVPluginResult + resultWithStatus:CDVCommandStatus_OK]; + + [self.commandDelegate sendPluginResult:result + callbackId:command.callbackId]; +} + +/** + * Restart playing sound when interrupted by phone calls. + */ +- (void) handleAudioSessionInterruption:(NSNotification*)notification +{ + [self fireEvent:kAPPBackgroundEventDeactivate]; + [self keepAwake]; +} + +/** + * Find out if the app runs inside the webkit powered webview. + */ ++ (BOOL) isRunningWebKit +{ + return IsAtLeastiOSVersion(@"8.0") && NSClassFromString(@"CDVWKWebViewEngine"); +} + +/** + * Method to fire an event with some parameters in the browser. + */ +- (void) fireEvent:(NSString*)event +{ + NSString* active = + [event isEqualToString:kAPPBackgroundEventActivate] ? @"true" : @"false"; + + NSString* flag = [NSString stringWithFormat:@"%@._isActive=%@;", + kAPPBackgroundJsNamespace, active]; + + NSString* depFn = [NSString stringWithFormat:@"%@.on%@();", + kAPPBackgroundJsNamespace, event]; + + NSString* fn = [NSString stringWithFormat:@"%@.fireEvent('%@');", + kAPPBackgroundJsNamespace, event]; + + NSString* js = [NSString stringWithFormat:@"%@%@%@", flag, depFn, fn]; + + [self.commandDelegate evalJs:js]; +} + +#pragma mark - +#pragma mark Swizzling + +/** + * Method to swizzle. + */ ++ (NSString*) wkProperty +{ + NSString* str = @"YWx3YXlzUnVuc0F0Rm9yZWdyb3VuZFByaW9yaXR5"; + NSData* data = [[NSData alloc] initWithBase64EncodedString:str options:0]; + + return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; +} + +/** + * Swizzle some implementations of CDVWKWebViewEngine. + */ ++ (void) swizzleWKWebViewEngine +{ + if (![self isRunningWebKit]) + return; + + Class wkWebViewEngineCls = NSClassFromString(@"CDVWKWebViewEngine"); + SEL selector = NSSelectorFromString(@"createConfigurationFromSettings:"); + + SwizzleSelectorWithBlock_Begin(wkWebViewEngineCls, selector) + ^(CDVPlugin *self, NSDictionary *settings) { + id obj = ((id (*)(id, SEL, NSDictionary*))_imp)(self, _cmd, settings); + + [obj setValue:[NSNumber numberWithBool:YES] + forKey:[APPBackgroundMode wkProperty]]; + + [obj setValue:[NSNumber numberWithBool:NO] + forKey:@"requiresUserActionForMediaPlayback"]; + + return obj; + } + SwizzleSelectorWithBlock_End; +} + +@end diff --git a/src/ios/APPMethodMagic.h b/src/ios/APPMethodMagic.h new file mode 100644 index 0000000..84de791 --- /dev/null +++ b/src/ios/APPMethodMagic.h @@ -0,0 +1,93 @@ +/* + Copyright 2013-2017 appPlant GmbH + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ + +/** + * Code extracted from + * - http://defagos.github.io/yet_another_article_about_method_swizzling/ + * - https://gist.github.com/defagos/1312fec96b48540efa5c + */ + +#import +#import + +#define SwizzleSelector(clazz, selector, newImpl, oldImpl) \ +(*oldImpl) = (__typeof((*oldImpl)))class_swizzleSelector((clazz), (selector), (IMP)(newImpl)) + +#define SwizzleClassSelector(clazz, selector, newImpl, oldImpl) \ +(*oldImpl) = (__typeof((*oldImpl)))class_swizzleClassSelector((clazz), (selector), (IMP)(newImpl)) + +#define SwizzleSelectorWithBlock_Begin(clazz, selector) { \ +SEL _cmd = selector; \ +__block IMP _imp = class_swizzleSelectorWithBlock((clazz), (selector), +#define SwizzleSelectorWithBlock_End );} + +#define SwizzleClassSelectorWithBlock_Begin(clazz, selector) { \ +SEL _cmd = selector; \ +__block IMP _imp = class_swizzleClassSelectorWithBlock((clazz), (selector), +#define SwizzleClassSelectorWithBlock_End );} + +/** + * Swizzle class method specified by class and selector + * through the provided method implementation. + * + * @param [ Class ] clazz The class containing the method. + * @param [ SEL ] selector The selector of the method. + * @param [ IMP ] newImpl The new implementation of the method. + * + * @return [ IMP ] The previous implementation of the method. + */ +IMP class_swizzleClassSelector(Class clazz, SEL selector, IMP newImpl); + +/** + * Swizzle class method specified by class and selector + * through the provided code block. + * + * @param [ Class ] clazz The class containing the method. + * @param [ SEL ] selector The selector of the method. + * @param [ id ] newImplBlock The new implementation of the method. + * + * @return [ IMP ] The previous implementation of the method. + */ +IMP class_swizzleClassSelectorWithBlock(Class clazz, SEL selector, id newImplBlock); + +/** + * Swizzle method specified by class and selector + * through the provided code block. + * + * @param [ Class ] clazz The class containing the method. + * @param [ SEL ] selector The selector of the method. + * @param [ id ] newImplBlock The new implementation of the method. + * + * @return [ IMP ] The previous implementation of the method. + */ +IMP class_swizzleSelectorWithBlock(Class clazz, SEL selector, id newImplBlock); + +/** + * Swizzle method specified by class and selector + * through the provided method implementation. + * + * @param [ Class ] clazz The class containing the method. + * @param [ SEL ] selector The selector of the method. + * @param [ IMP ] newImpl The new implementation of the method. + * + * @return [ IMP ] The previous implementation of the method. + */ +IMP class_swizzleSelector(Class clazz, SEL selector, IMP newImpl); diff --git a/src/ios/APPMethodMagic.m b/src/ios/APPMethodMagic.m new file mode 100644 index 0000000..cb9cc35 --- /dev/null +++ b/src/ios/APPMethodMagic.m @@ -0,0 +1,105 @@ +/* + Copyright 2013-2017 appPlant GmbH + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ + +/** + * Code extracted from + * - http://defagos.github.io/yet_another_article_about_method_swizzling/ + * - https://gist.github.com/defagos/1312fec96b48540efa5c + */ + +#import "APPMethodMagic.h" +#import +#import + +/** + * Swizzle class method specified by class and selector + * through the provided method implementation. + * + * @param [ Class ] clazz The class containing the method. + * @param [ SEL ] selector The selector of the method. + * @param [ IMP ] newImpl The new implementation of the method. + * + * @return [ IMP ] The previous implementation of the method. + */ +IMP class_swizzleClassSelector(Class clazz, SEL selector, IMP newImpl) +{ + return class_swizzleSelector(object_getClass(clazz), selector, newImpl); +} + +/** + * Swizzle class method specified by class and selector + * through the provided code block. + * + * @param [ Class ] clazz The class containing the method. + * @param [ SEL ] selector The selector of the method. + * @param [ id ] newImplBlock The new implementation of the method. + * + * @return [ IMP ] The previous implementation of the method. + */ +IMP class_swizzleClassSelectorWithBlock(Class clazz, SEL selector, id newImplBlock) +{ + IMP newImpl = imp_implementationWithBlock(newImplBlock); + return class_swizzleClassSelector(clazz, selector, newImpl); +} + +/** + * Swizzle method specified by class and selector + * through the provided code block. + * + * @param [ Class ] clazz The class containing the method. + * @param [ SEL ] selector The selector of the method. + * @param [ id ] newImplBlock The new implementation of the method. + * + * @return [ IMP ] The previous implementation of the method. + */ +IMP class_swizzleSelectorWithBlock(Class clazz, SEL selector, id newImplBlock) +{ + IMP newImpl = imp_implementationWithBlock(newImplBlock); + return class_swizzleSelector(clazz, selector, newImpl); +} + +/** + * Swizzle method specified by class and selector + * through the provided method implementation. + * + * @param [ Class ] clazz The class containing the method. + * @param [ SEL ] selector The selector of the method. + * @param [ IMP ] newImpl The new implementation of the method. + * + * @return [ IMP ] The previous implementation of the method. + */ +IMP class_swizzleSelector(Class clazz, SEL selector, IMP newImpl) +{ + Method method = class_getInstanceMethod(clazz, selector); + const char *types = method_getTypeEncoding(method); + + class_addMethod(clazz, selector, imp_implementationWithBlock(^(__unsafe_unretained id self, va_list argp) { + struct objc_super super = { + .receiver = self, + .super_class = class_getSuperclass(clazz) + }; + + id (*objc_msgSendSuper_typed)(struct objc_super*, SEL, va_list) = (void*)&objc_msgSendSuper; + return objc_msgSendSuper_typed(&super, selector, argp); + }), types); + + return class_replaceMethod(clazz, selector, newImpl, types); +} diff --git a/src/windows/BackgroundModeProxy.js b/src/windows/BackgroundModeProxy.js new file mode 100644 index 0000000..5542e3f --- /dev/null +++ b/src/windows/BackgroundModeProxy.js @@ -0,0 +1,123 @@ +/* + Copyright 2013-2017 appPlant GmbH + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +var plugin = cordova.plugins.backgroundMode; + +var Uri = Windows.Foundation.Uri, + MediaSource = Windows.Media.Core.MediaSource, + MediaPlaybackItem = Windows.Media.Playback.MediaPlaybackItem, + MediaPlaybackList = Windows.Media.Playback.MediaPlaybackList, + AudioCategory = Windows.Media.Playback.MediaPlayerAudioCategory, + MediaPlayer = Windows.Media.Playback.MediaPlayer, + WebUIApplication = Windows.UI.WebUI.WebUIApplication; + +/** + * Activates the background mode. When activated the application + * will be prevented from going to sleep while in background + * for the next time. + * + * @param [ Function ] success The success callback to use. + * @param [ Function ] error The error callback to use. + * + * @return [ Void ] + */ +exports.enable = function (success, error) { + success(); +}; + +/** + * Deactivates the background mode. When deactivated the application + * will not stay awake while in background. + * + * @param [ Function ] success The success callback to use. + * @param [ Function ] error The error callback to use. + * + * @return [ Void ] + */ +exports.disable = function (success, error) { + exports.stopKeepingAwake(); + success(); +}; + +/** + * Keep the app awake. + * + * @return [ Void ] + */ +exports.keepAwake = function () { + if (!plugin.isEnabled() || plugin.isActive()) + return; + + exports.configureAudioPlayer(); + exports.audioPlayer.play(); + + plugin._isActive = true; + plugin.fireEvent('activate'); +}; + +/** + * Let the app going to sleep. + * + * @return [ Void ] + */ +exports.stopKeepingAwake = function () { + if (!exports.audioPlayer) + return; + + exports.audioPlayer.close(); + exports.audioPlayer = null; + + cordova.plugins.backgroundMode._isActive = false; + cordova.plugins.backgroundMode.fireEvent('deactivate'); +}; + +/** + * Configure the audio player for playback in background. + * + * @return [ Void ] + */ +exports.configureAudioPlayer = function () { + if (exports.audioPlayer) + return; + + var pkg = Windows.ApplicationModel.Package.current, + pkgName = pkg.id.name; + + var audioPlayer = new MediaPlayer(), + audioFile = new Uri('ms-appx://' + pkgName + '/appbeep.wma'), + audioSource = MediaSource.createFromUri(audioFile), + playList = new MediaPlaybackList(); + + playList.items.append(new MediaPlaybackItem(audioSource)); + playList.autoRepeatEnabled = true; + + audioPlayer.source = playList; + audioPlayer.autoPlay = false; + audioPlayer.audioCategory = AudioCategory.soundEffects; + audioPlayer.volume = 0; + + exports.audioPlayer = audioPlayer; +}; + +WebUIApplication.addEventListener('enteredbackground', exports.keepAwake, false); +WebUIApplication.addEventListener('leavingbackground', exports.stopKeepingAwake, false); + +cordova.commandProxy.add('BackgroundMode', exports); diff --git a/www/background-mode.js b/www/background-mode.js new file mode 100644 index 0000000..7dcdff3 --- /dev/null +++ b/www/background-mode.js @@ -0,0 +1,403 @@ +/* + Copyright 2013-2017 appPlant GmbH + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ + +var exec = require('cordova/exec'), + channel = require('cordova/channel'); + + +/************* + * INTERFACE * + *************/ + +/** + * Activates the background mode. When activated the application + * will be prevented from going to sleep while in background + * for the next time. + * + * @return [ Void ] + */ +exports.enable = function () { + if (this.isEnabled()) + return; + + var fn = function () { + exports._isEnabled = true; + exports.fireEvent('enable'); + }; + + cordova.exec(fn, null, 'BackgroundMode', 'enable', []); +}; + +/** + * Deactivates the background mode. When deactivated the application + * will not stay awake while in background. + * + * @return [ Void ] + */ +exports.disable = function () { + if (!this.isEnabled()) + return; + + var fn = function () { + exports._isEnabled = false; + exports.fireEvent('disable'); + }; + + cordova.exec(fn, null, 'BackgroundMode', 'disable', []); +}; + +/** + * Enable or disable the background mode. + * + * @param [ Bool ] enable The status to set for. + * + * @return [ Void ] + */ +exports.setEnabled = function (enable) { + if (enable) { + this.enable(); + } else { + this.disable(); + } +}; + +/** + * List of all available options with their default value. + * + * @return [ Object ] + */ +exports.getDefaults = function () { + return this._defaults; +}; + +/** + * Overwrite the default settings. + * + * @param [ Object ] overrides Dict of options to be overridden. + * + * @return [ Void ] + */ +exports.setDefaults = function (overrides) { + var defaults = this.getDefaults(); + + for (var key in defaults) { + if (overrides.hasOwnProperty(key)) { + defaults[key] = overrides[key]; + } + } + + if (this._isAndroid) { + cordova.exec(null, null, 'BackgroundMode', 'configure', [defaults, false]); + } +}; + +/** + * Configures the notification settings for Android. + * Will be merged with the defaults. + * + * @param [ Object ] overrides Dict of options to be overridden. + * + * @return [ Void ] + */ +exports.configure = function (options) { + var settings = this.mergeWithDefaults(options); + + if (this._isAndroid) { + cordova.exec(null, null, 'BackgroundMode', 'configure', [settings, true]); + } +}; + +/** + * Enable GPS-tracking in background (Android). + * + * @return [ Void ] + */ +exports.disableWebViewOptimizations = function () { + if (this._isAndroid) { + cordova.exec(null, null, 'BackgroundMode', 'optimizations', []); + } +}; + +/** + * Move app to background (Android only). + * + * @return [ Void ] + */ +exports.moveToBackground = function () { + if (this._isAndroid) { + cordova.exec(null, null, 'BackgroundMode', 'background', []); + } +}; + +/** + * Move app to foreground when in background (Android only). + * + * @return [ Void ] + */ +exports.moveToForeground = function () { + if (this.isActive() && this._isAndroid) { + cordova.exec(null, null, 'BackgroundMode', 'foreground', []); + } +}; + +/** + * Exclude the app from the recent tasks list (Android only). + * + * @return [ Void ] + */ +exports.excludeFromTaskList = function () { + if (this._isAndroid) { + cordova.exec(null, null, 'BackgroundMode', 'tasklist', []); + } +}; + +/** + * Override the back button on Android to go to background + * instead of closing the app. + * + * @return [ Void ] + */ +exports.overrideBackButton = function () { + document.addEventListener('backbutton', function() { + exports.moveToBackground(); + }, false); +}; + +/** + * If the mode is enabled or disabled. + * + * @return [ Boolean ] + */ +exports.isEnabled = function () { + return this._isEnabled !== false; +}; + +/** + * If the mode is active. + * + * @return [ Boolean ] + */ +exports.isActive = function () { + return this._isActive !== false; +}; + + +/********** + * EVENTS * + **********/ + +exports._listener = {}; + +/** + * Fire event with given arguments. + * + * @param [ String ] event The event's name. + * @param [ Array ] The callback's arguments. + * + * @return [ Void ] + */ +exports.fireEvent = function (event) { + var args = Array.apply(null, arguments).slice(1), + listener = this._listener[event]; + + if (!listener) + return; + + for (var i = 0; i < listener.length; i++) { + var fn = listener[i][0], + scope = listener[i][1]; + + fn.apply(scope, args); + } +}; + +/** + * Register callback for given event. + * + * @param [ String ] event The event's name. + * @param [ Function ] callback The function to be exec as callback. + * @param [ Object ] scope The callback function's scope. + * + * @return [ Void ] + */ +exports.on = function (event, callback, scope) { + + if (typeof callback !== "function") + return; + + if (!this._listener[event]) { + this._listener[event] = []; + } + + var item = [callback, scope || window]; + + this._listener[event].push(item); +}; + +/** + * Unregister callback for given event. + * + * @param [ String ] event The event's name. + * @param [ Function ] callback The function to be exec as callback. + * + * @return [ Void ] + */ +exports.un = function (event, callback) { + var listener = this._listener[event]; + + if (!listener) + return; + + for (var i = 0; i < listener.length; i++) { + var fn = listener[i][0]; + + if (fn == callback) { + listener.splice(i, 1); + break; + } + } +}; + +/** + * @deprecated + * + * Called when the background mode has been activated. + */ +exports.onactivate = function () {}; + +/** + * @deprecated + * + * Called when the background mode has been deaktivated. + */ +exports.ondeactivate = function () {}; + +/** + * @deprecated + * + * Called when the background mode could not been activated. + * + * @param {Integer} errorCode + * Error code which describes the error + */ +exports.onfailure = function () {}; + + +/********* + * UTILS * + *********/ + +/** + * @private + * + * Merge settings with default values. + * + * @param [ Object ] options The custom options. + * + * @return [ Object ] Default values merged with custom values. + */ +exports.mergeWithDefaults = function (options) { + var defaults = this.getDefaults(); + + for (var key in defaults) { + if (!options.hasOwnProperty(key)) { + options[key] = defaults[key]; + continue; + } + } + + return options; +}; + +/** + * @private + * + * Initialize the plugin. + * + * Method should be called after the 'deviceready' event + * but before the event listeners will be called. + * + * @return [ Void ] + */ +exports.pluginInitialize = function () { + this._isAndroid = device.platform.match(/^android|amazon/i) !== null; + this.setDefaults({}); + + if (device.platform == 'browser') { + this.enable(); + this._isEnabled = true; + } + + this._isActive = this._isActive || device.platform == 'browser'; +}; + + +/*********** + * PRIVATE * + ***********/ + +/** + * @private + * + * Flag indicates if the mode is enabled. + */ +exports._isEnabled = false; + +/** + * @private + * + * Flag indicates if the mode is active. + */ +exports._isActive = false; + +/** + * @private + * + * Default values of all available options. + */ +exports._defaults = { + title: 'App is running in background', + text: 'Doing heavy tasks.', + bigText: false, + resume: true, + silent: false, + hidden: true, + color: undefined, + icon: 'icon' +}; + +// Called before 'deviceready' listener will be called +channel.onCordovaReady.subscribe(function () { + channel.onCordovaInfoReady.subscribe(function () { + exports.pluginInitialize(); + }); +}); + +// Called after 'deviceready' event +channel.deviceready.subscribe(function () { + if (exports.isEnabled()) { + exports.fireEvent('enable'); + } + + if (exports.isActive()) { + exports.fireEvent('activate'); + } +});