diff --git a/src/android/BackgroundExt.java b/src/android/BackgroundExt.java index ad3b9ea..9f27859 100644 --- a/src/android/BackgroundExt.java +++ b/src/android/BackgroundExt.java @@ -30,7 +30,6 @@ import android.content.Intent; import android.os.Build; import android.os.PowerManager; import android.view.View; -import android.view.Window; import org.apache.cordova.CallbackContext; import org.apache.cordova.CordovaInterface; @@ -39,24 +38,29 @@ import org.apache.cordova.CordovaWebView; import org.apache.cordova.PluginResult; import org.apache.cordova.PluginResult.Status; -import java.lang.ref.WeakReference; import java.util.List; import static android.content.Context.ACTIVITY_SERVICE; import static android.content.Context.POWER_SERVICE; +import static android.os.Build.VERSION.SDK_INT; import static android.view.WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON; import static android.view.WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD; import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED; import static android.view.WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON; +/** + * Implements extended functions around the main purpose + * of infinite execution in the background. + */ class BackgroundExt { - // Weak reference to the cordova interface passed by the plugin - private final WeakReference cordova; + // Reference to the cordova interface passed by the plugin + private final CordovaInterface cordova; - // Weak reference to the cordova web view passed by the plugin - private final WeakReference webView; + // Reference to the cordova web view passed by the plugin + private final CordovaWebView webView; + // To keep the device awake private PowerManager.WakeLock wakeLock; /** @@ -64,35 +68,24 @@ class BackgroundExt { * * @param plugin The cordova plugin. */ - private BackgroundExt(CordovaPlugin plugin) { - this.cordova = new WeakReference(plugin.cordova); - this.webView = new WeakReference(plugin.webView); + BackgroundExt(CordovaPlugin plugin) + { + this.cordova = plugin.cordova; + this.webView = plugin.webView; } /** - * Executes the request asynchronous. + * Executes the request within a thread. * - * @param plugin The cordova plugin. * @param action The action to execute. * @param callback The callback context used when * calling back into JavaScript. */ - @SuppressWarnings("UnusedParameters") - static void execute (CordovaPlugin plugin, final String action, - final CallbackContext callback) { - - final BackgroundExt ext = new BackgroundExt(plugin); - - plugin.cordova.getThreadPool().execute(new Runnable() { - @Override - public void run() { - ext.execute(action, callback); - } - }); + void executeAsync (String action, CallbackContext callback) + { + cordova.getThreadPool().execute(() -> execute(action, callback)); } - // codebeat:disable[ABC] - /** * Executes the request. * @@ -100,60 +93,59 @@ class BackgroundExt { * @param callback The callback context used when * calling back into JavaScript. */ - private void execute (String action, CallbackContext callback) { - - if (action.equalsIgnoreCase("optimizations")) { - disableWebViewOptimizations(); - } - - if (action.equalsIgnoreCase("background")) { - moveToBackground(); - } - - if (action.equalsIgnoreCase("foreground")) { - moveToForeground(); - } - - if (action.equalsIgnoreCase("tasklist")) { - excludeFromTaskList(); - } - - if (action.equalsIgnoreCase("dimmed")) { - isDimmed(callback); - } - - if (action.equalsIgnoreCase("wakeup")) { - wakeup(); - } - - if (action.equalsIgnoreCase("unlock")) { - wakeup(); - unlock(); + private void execute (String action, CallbackContext callback) + { + switch (action) + { + case "optimizations": + disableWebViewOptimizations(); + break; + case "background": + moveToBackground(); + break; + case "foreground": + moveToForeground(); + break; + case "tasklist": + excludeFromTaskList(); + break; + case "dimmed": + isDimmed(callback); + break; + case "wakeup": + wakeup(); + break; + case "unlock": + wakeup(); + unlock(); + break; } } - // codebeat:enable[ABC] - /** - * Move app to background. + * Moves the app to the background. */ - private void moveToBackground() { + private void moveToBackground() + { Intent intent = new Intent(Intent.ACTION_MAIN); intent.addCategory(Intent.CATEGORY_HOME); + getApp().startActivity(intent); } /** - * Move app to foreground. + * Moves the app to the foreground. */ - private void moveToForeground() { + private void moveToForeground() + { Activity app = getApp(); Intent intent = getLaunchIntent(); intent.addFlags( Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | - Intent.FLAG_ACTIVITY_SINGLE_TOP); + Intent.FLAG_ACTIVITY_SINGLE_TOP | + Intent.FLAG_ACTIVITY_CLEAR_TOP); app.startActivity(intent); } @@ -166,18 +158,15 @@ class BackgroundExt { public void run() { try { Thread.sleep(1000); - getApp().runOnUiThread(new Runnable() { - @Override - public void run() { - View view = webView.get().getEngine().getView(); + getApp().runOnUiThread(() -> { + View view = webView.getEngine().getView(); - try { - Class.forName("org.crosswalk.engine.XWalkCordovaView") - .getMethod("onShow") - .invoke(view); - } catch (Exception e){ - view.dispatchWindowVisibilityChanged(View.VISIBLE); - } + try { + Class.forName("org.crosswalk.engine.XWalkCordovaView") + .getMethod("onShow") + .invoke(view); + } catch (Exception e){ + view.dispatchWindowVisibilityChanged(View.VISIBLE); } }); } catch (InterruptedException e) { @@ -190,13 +179,14 @@ class BackgroundExt { } /** - * Exclude the app from the recent tasks list. + * Excludes the app from the recent tasks list. */ @TargetApi(Build.VERSION_CODES.LOLLIPOP) - private void excludeFromTaskList() { + private void excludeFromTaskList() + { ActivityManager am = (ActivityManager) getService(ACTIVITY_SERVICE); - if (am == null || Build.VERSION.SDK_INT < 21) + if (am == null || SDK_INT < 21) return; List tasks = am.getAppTasks(); @@ -208,24 +198,29 @@ class BackgroundExt { } /** - * Invoke the callback with information if the screen is on. + * Invokes the callback with information if the screen is on. * * @param callback The callback to invoke. */ @SuppressWarnings("deprecation") - private void isDimmed(CallbackContext callback) { - PluginResult result = new PluginResult(Status.OK, isDimmed()); - callback.sendPluginResult(result); + private void isDimmed (CallbackContext callback) + { + boolean status = isDimmed(); + PluginResult res = new PluginResult(Status.OK, status); + + callback.sendPluginResult(res); } /** - * If the screen is active. + * Returns if the screen is active. */ @SuppressWarnings("deprecation") - private boolean isDimmed() { + private boolean isDimmed() + { PowerManager pm = (PowerManager) getService(POWER_SERVICE); - if (Build.VERSION.SDK_INT < 20) { + if (SDK_INT < 20) + { return !pm.isScreenOn(); } @@ -235,7 +230,8 @@ class BackgroundExt { /** * Wakes up the device if the screen isn't still on. */ - private void wakeup() { + private void wakeup() + { try { acquireWakeLock(); } catch (Exception e) { @@ -246,27 +242,31 @@ class BackgroundExt { /** * Unlocks the device even with password protection. */ - private void unlock() { - Intent intent = getLaunchIntent(); - getApp().startActivity(intent); + private void unlock() + { + getApp().runOnUiThread(() -> { + addSreenAndKeyguardFlags(); + getApp().startActivity(getLaunchIntent()); + }); } /** - * Acquire a wake lock to wake up the device. + * Acquires a wake lock to wake up the device. */ - private void acquireWakeLock() { + @SuppressWarnings("deprecation") + private void acquireWakeLock() + { PowerManager pm = (PowerManager) getService(POWER_SERVICE); releaseWakeLock(); - if (!isDimmed()) { + if (!isDimmed()) return; - } int level = PowerManager.SCREEN_DIM_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP; - wakeLock = pm.newWakeLock(level, "BackgroundModeExt"); + wakeLock = pm.newWakeLock(level, "backgroundmode:wakelock"); wakeLock.setReferenceCounted(false); wakeLock.acquire(1000); } @@ -274,7 +274,8 @@ class BackgroundExt { /** * Releases the previously acquire wake lock. */ - private void releaseWakeLock() { + private void releaseWakeLock() + { if (wakeLock != null && wakeLock.isHeld()) { wakeLock.release(); wakeLock = null; @@ -282,36 +283,33 @@ class BackgroundExt { } /** - * Add required flags to the window to unlock/wakeup the device. + * Adds required flags to the window to unlock/wakeup the device. */ - static void addWindowFlags(Activity app) { - final Window window = app.getWindow(); - - app.runOnUiThread(new Runnable() { - public void run() { - window.addFlags( - FLAG_ALLOW_LOCK_WHILE_SCREEN_ON | - FLAG_SHOW_WHEN_LOCKED | - FLAG_TURN_SCREEN_ON | - FLAG_DISMISS_KEYGUARD - ); - } - }); + private void addSreenAndKeyguardFlags() + { + getApp().getWindow().addFlags(FLAG_ALLOW_LOCK_WHILE_SCREEN_ON | FLAG_SHOW_WHEN_LOCKED | FLAG_TURN_SCREEN_ON | FLAG_DISMISS_KEYGUARD); } /** - * The activity referenced by cordova. - * - * @return The main activity of the app. + * Removes required flags to the window to unlock/wakeup the device. + */ + static void clearKeyguardFlags (Activity app) + { + app.runOnUiThread(() -> app.getWindow().clearFlags(FLAG_DISMISS_KEYGUARD)); + } + + /** + * Returns the activity referenced by cordova. */ Activity getApp() { - return cordova.get().getActivity(); + return cordova.getActivity(); } /** - * The launch intent for the main activity. + * Gets the launch intent for the main activity. */ - private Intent getLaunchIntent() { + private Intent getLaunchIntent() + { Context app = getApp().getApplicationContext(); String pkgName = app.getPackageName(); @@ -322,11 +320,9 @@ class BackgroundExt { * Get the requested system service by name. * * @param name The name of the service. - * - * @return The service instance. */ - private Object getService(String name) { + private Object getService(String name) + { return getApp().getSystemService(name); } - } diff --git a/src/android/BackgroundMode.java b/src/android/BackgroundMode.java index 3ade663..5279e28 100644 --- a/src/android/BackgroundMode.java +++ b/src/android/BackgroundMode.java @@ -30,23 +30,20 @@ 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; import de.appplant.cordova.plugin.background.ForegroundService.ForegroundBinder; import static android.content.Context.BIND_AUTO_CREATE; +import static de.appplant.cordova.plugin.background.BackgroundExt.clearKeyguardFlags; public class BackgroundMode extends CordovaPlugin { // Event types for callbacks - private enum Event { - ACTIVATE, DEACTIVATE, FAILURE - } + private enum Event { ACTIVATE, DEACTIVATE, FAILURE } // Plugin namespace - private static final String JS_NAMESPACE = - "cordova.plugins.backgroundMode"; + private static final String JS_NAMESPACE = "cordova.plugins.backgroundMode"; // Flag indicates if the app is in background or foreground private boolean inBackground = false; @@ -64,26 +61,22 @@ public class BackgroundMode extends CordovaPlugin { private ForegroundService service; // Used to (un)bind the service to with the activity - private final ServiceConnection connection = new ServiceConnection() { + private final ServiceConnection connection = new ServiceConnection() + { @Override - public void onServiceConnected(ComponentName name, IBinder service) { + public void onServiceConnected (ComponentName name, IBinder service) + { ForegroundBinder binder = (ForegroundBinder) service; BackgroundMode.this.service = binder.getService(); } @Override - public void onServiceDisconnected(ComponentName name) { + public void onServiceDisconnected (ComponentName name) + { fireEvent(Event.FAILURE, "'service disconnected'"); } }; - @Override - protected void pluginInitialize() { - BackgroundExt.addWindowFlags(cordova.getActivity()); - } - - // codebeat:disable[ABC] - /** * Executes the request. * @@ -93,47 +86,68 @@ public class BackgroundMode extends CordovaPlugin { * 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 { + CallbackContext callback) + { + boolean validAction = true; - if (action.equalsIgnoreCase("configure")) { - configure(args.getJSONObject(0), args.getBoolean(1)); - callback.success(); - return true; + switch (action) + { + case "configure": + configure(args.optJSONObject(0), args.optBoolean(1)); + break; + case "enable": + enableMode(); + break; + case "disable": + disableMode(); + break; + case "optimizations": + case "background": + case "foreground": + case "tasklist": + case "dimmed": + case "wakeup": + case "unlock": + new BackgroundExt(this).executeAsync(action, callback); + break; + default: + validAction = false; } - if (action.equalsIgnoreCase("enable")) { - enableMode(); + if (validAction) { callback.success(); - return true; + } else { + callback.error("Invalid action: " + action); } - if (action.equalsIgnoreCase("disable")) { - disableMode(); - callback.success(); - return true; - } - - BackgroundExt.execute(this, action, callback); - return true; + return validAction; } - // codebeat:enable[ABC] - /** * 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(); + public void onPause(boolean multitasking) + { + try { + inBackground = true; + startService(); + } finally { + clearKeyguardFlags(cordova.getActivity()); + } + } + + /** + * Called when the activity is no longer visible to the user. + */ + @Override + public void onStop () { + clearKeyguardFlags(cordova.getActivity()); } /** @@ -142,8 +156,8 @@ public class BackgroundMode extends CordovaPlugin { * @param multitasking Flag indicating if multitasking is turned on for app. */ @Override - public void onResume(boolean multitasking) { - super.onResume(multitasking); + public void onResume (boolean multitasking) + { inBackground = false; stopService(); } @@ -152,16 +166,17 @@ public class BackgroundMode extends CordovaPlugin { * Called when the activity will be destroyed. */ @Override - public void onDestroy() { + public void onDestroy() + { stopService(); - super.onDestroy(); android.os.Process.killProcess(android.os.Process.myPid()); } /** * Enable the background mode. */ - private void enableMode() { + private void enableMode() + { isDisabled = false; if (inBackground) { @@ -172,7 +187,8 @@ public class BackgroundMode extends CordovaPlugin { /** * Disable the background mode. */ - private void disableMode() { + private void disableMode() + { stopService(); isDisabled = true; } @@ -183,7 +199,8 @@ public class BackgroundMode extends CordovaPlugin { * @param settings The settings * @param update A truthy value means to update the running service. */ - private void configure(JSONObject settings, boolean update) { + private void configure(JSONObject settings, boolean update) + { if (update) { updateNotification(settings); } else { @@ -196,17 +213,15 @@ public class BackgroundMode extends CordovaPlugin { * * @param settings The new default settings */ - private void setDefaultSettings(JSONObject settings) { + private void setDefaultSettings(JSONObject settings) + { defaultSettings = settings; } /** - * The settings for the new/updated notification. - * - * @return - * updateSettings if set or default settings + * Returns the settings for the new/updated notification. */ - protected static JSONObject getSettings() { + static JSONObject getSettings () { return defaultSettings; } @@ -215,7 +230,8 @@ public class BackgroundMode extends CordovaPlugin { * * @param settings The config settings */ - private void updateNotification(JSONObject settings) { + private void updateNotification(JSONObject settings) + { if (isBind) { service.updateNotification(settings); } @@ -225,7 +241,8 @@ public class BackgroundMode extends CordovaPlugin { * Bind the activity to a background service and put them into foreground * state. */ - private void startService() { + private void startService() + { Activity context = cordova.getActivity(); if (isDisabled || isBind) @@ -248,12 +265,12 @@ public class BackgroundMode extends CordovaPlugin { * Bind the activity to a background service and put them into foreground * state. */ - private void stopService() { + private void stopService() + { Activity context = cordova.getActivity(); Intent intent = new Intent(context, ForegroundService.class); - if (!isBind) - return; + if (!isBind) return; fireEvent(Event.DEACTIVATE, null); context.unbindService(connection); @@ -268,7 +285,8 @@ public class BackgroundMode extends CordovaPlugin { * @param event The name of the event * @param params Optional arguments for the event */ - private void fireEvent (Event event, String params) { + private void fireEvent (Event event, String params) + { String eventName = event.name().toLowerCase(); Boolean active = event == Event.ACTIVATE; @@ -283,12 +301,7 @@ public class BackgroundMode extends CordovaPlugin { final String js = str; - cordova.getActivity().runOnUiThread(new Runnable() { - @Override - public void run() { - webView.loadUrl("javascript:" + js); - } - }); + cordova.getActivity().runOnUiThread(() -> webView.loadUrl("javascript:" + js)); } } diff --git a/src/android/ForegroundService.java b/src/android/ForegroundService.java index 544b4fb..0544192 100644 --- a/src/android/ForegroundService.java +++ b/src/android/ForegroundService.java @@ -1,5 +1,6 @@ /* 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 @@ -7,7 +8,9 @@ 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 @@ -18,6 +21,7 @@ package de.appplant.cordova.plugin.background; +import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Notification; import android.app.NotificationManager; @@ -58,26 +62,27 @@ public class ForegroundService extends Service { private static final String NOTIFICATION_ICON = "icon"; // Binder given to clients - private final IBinder mBinder = new ForegroundBinder(); + private final IBinder binder = new ForegroundBinder(); // Partial wake lock to prevent the app from going to sleep when locked private PowerManager.WakeLock wakeLock; - private final String CHANNEL_ID = "cordova-plugin-background-mode-id"; /** * Allow clients to call on to the service. */ @Override public IBinder onBind (Intent intent) { - return mBinder; + 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. */ - public class ForegroundBinder extends Binder { - ForegroundService getService() { + class ForegroundBinder extends Binder + { + ForegroundService getService() + { // Return this instance of ForegroundService // so clients can call public methods return ForegroundService.this; @@ -89,7 +94,8 @@ public class ForegroundService extends Service { * by the OS. */ @Override - public void onCreate () { + public void onCreate() + { super.onCreate(); keepAwake(); } @@ -98,16 +104,27 @@ public class ForegroundService extends Service { * No need to run headless on destroy. */ @Override - public void onDestroy() { + 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. */ - private void keepAwake() { + @SuppressLint("WakelockTimeout") + private void keepAwake() + { JSONObject settings = BackgroundMode.getSettings(); boolean isSilent = settings.optBoolean("silent", false); @@ -115,11 +132,10 @@ public class ForegroundService extends Service { startForeground(NOTIFICATION_ID, makeNotification()); } - PowerManager pm = (PowerManager) - getSystemService(POWER_SERVICE); + PowerManager pm = (PowerManager)getSystemService(POWER_SERVICE); wakeLock = pm.newWakeLock( - PARTIAL_WAKE_LOCK, "BackgroundMode"); + PARTIAL_WAKE_LOCK, "backgroundmode:wakelock"); wakeLock.acquire(); } @@ -127,7 +143,8 @@ public class ForegroundService extends Service { /** * Stop background mode. */ - private void sleepWell() { + private void sleepWell() + { stopForeground(true); getNotificationManager().cancel(NOTIFICATION_ID); @@ -141,7 +158,8 @@ public class ForegroundService extends Service { * 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() { + private Notification makeNotification() + { return makeNotification(BackgroundMode.getSettings()); } @@ -151,22 +169,24 @@ public class ForegroundService extends Service { * * @param settings The config settings */ - private Notification makeNotification(JSONObject settings) { + private Notification makeNotification (JSONObject settings) + { // use channelid for Oreo and higher - 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"; + 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; + int importance = NotificationManager.IMPORTANCE_LOW; - NotificationChannel mChannel = new NotificationChannel(this.CHANNEL_ID, name,importance); + NotificationChannel mChannel = new NotificationChannel(CHANNEL_ID, name,importance); - // Configure the notification channel. - mChannel.setDescription(description); + // Configure the notification channel. + mChannel.setDescription(description); - getNotificationManager().createNotificationChannel(mChannel); + getNotificationManager().createNotificationChannel(mChannel); } String title = settings.optString("title", NOTIFICATION_TITLE); String text = settings.optString("text", NOTIFICATION_TEXT); @@ -184,7 +204,7 @@ public class ForegroundService extends Service { .setSmallIcon(getIconResId(settings)); if(Build.VERSION.SDK_INT >= 26){ - notification.setChannelId(this.CHANNEL_ID); + notification.setChannelId(CHANNEL_ID); } if (settings.optBoolean("hidden", true)) { @@ -216,7 +236,8 @@ public class ForegroundService extends Service { * * @param settings The config settings */ - protected void updateNotification (JSONObject settings) { + protected void updateNotification (JSONObject settings) + { boolean isSilent = settings.optBoolean("silent", false); if (isSilent) { @@ -234,10 +255,10 @@ public class ForegroundService extends Service { * * @param settings A JSON dict containing the icon name. */ - private int getIconResId(JSONObject settings) { + private int getIconResId (JSONObject settings) + { String icon = settings.optString("icon", NOTIFICATION_ICON); - // cordova-android 6 uses mipmaps int resId = getIconResId(icon, "mipmap"); if (resId == 0) { @@ -255,7 +276,8 @@ public class ForegroundService extends Service { * * @return The resource id or 0 if not found. */ - private int getIconResId(String icon, String type) { + private int getIconResId (String icon, String type) + { Resources res = getResources(); String pkgName = getPackageName(); @@ -275,8 +297,8 @@ public class ForegroundService extends Service { * @param settings A JSON dict containing the color definition (red: FF0000) */ @TargetApi(Build.VERSION_CODES.LOLLIPOP) - private void setColor(Notification.Builder notification, - JSONObject settings) { + private void setColor (Notification.Builder notification, JSONObject settings) + { String hex = settings.optString("color", null); @@ -292,9 +314,10 @@ public class ForegroundService extends Service { } /** - * Shared manager for the notification service. + * Returns the shared notification service manager. */ - private NotificationManager getNotificationManager() { + private NotificationManager getNotificationManager() + { return (NotificationManager) getSystemService(NOTIFICATION_SERVICE); }