diff --git a/.idea/flutter_background_service_android.iml b/.idea/flutter_background_service_android.iml new file mode 100644 index 0000000..6155e93 --- /dev/null +++ b/.idea/flutter_background_service_android.iml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Dart_Packages.xml b/.idea/libraries/Dart_Packages.xml new file mode 100644 index 0000000..460f26e --- /dev/null +++ b/.idea/libraries/Dart_Packages.xml @@ -0,0 +1,236 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Dart_SDK.xml b/.idea/libraries/Dart_SDK.xml new file mode 100644 index 0000000..3dad229 --- /dev/null +++ b/.idea/libraries/Dart_SDK.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/libraries/Flutter_Plugins.xml b/.idea/libraries/Flutter_Plugins.xml new file mode 100644 index 0000000..b0f6971 --- /dev/null +++ b/.idea/libraries/Flutter_Plugins.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..aaa4363 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5064f81 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,180 @@ +## 2.0.3 + + - **FIX**: wakelock not released. ([e427f3b7](https://github.com/ekasetiawans/flutter_background_service/commit/e427f3b70138ec26f9671c2617f9061f25eade6f)) + +## 2.0.2 + + - **FIX**: autoStartOnBootMode #160. ([16a785a3](https://github.com/ekasetiawans/flutter_background_service/commit/16a785a3cbcb4226321ddddf681b6554196fa4db)) + +## 2.0.1 + + - **FIX**: release wakelock. ([c0830250](https://github.com/ekasetiawans/flutter_background_service/commit/c0830250b90a1ba6e2543a1bb25a13fba59a56b7)) + +## 2.0.0 + + - Graduate package to a stable release. See pre-releases prior to this version for changelog entries. + +## 2.0.0-dev.0 + +> Note: This release has breaking changes. + + - **BREAKING** **FEAT**: implement new concept. ([c8ce9c0b](https://github.com/ekasetiawans/flutter_background_service/commit/c8ce9c0bab82137dea031af124b84510286661f7)) + +## 1.0.2 + + - **DOCS**: readme link. ([1479b91c](https://github.com/ekasetiawans/flutter_background_service/commit/1479b91cd80d637335de1314a528bcf51ebb7c0f)) + +## 1.0.1 + + - **DOCS**: update README. ([fbf5e0ab](https://github.com/ekasetiawans/flutter_background_service/commit/fbf5e0abeeb9296ba32361b8af0a298ee9e71527)) + +## 0.0.2 + + - **FEAT**: migrate to plugin platform interface. ([70e08ff0](https://github.com/ekasetiawans/flutter_background_service/commit/70e08ff03232c31946cc8eb7896f69c830f23322)) + +## 0.0.1+3 + + - **FIX**: errors. ([13a6f841](https://github.com/ekasetiawans/flutter_background_service/commit/13a6f841f5d677ceb0010e8ba1bf9d7af53adbcf)) + +## 0.0.1+2 + + - Update a dependency to the latest release. + +## 0.0.1+1 + + - **REFACTOR**: initialize melos. + +## 0.2.6 +* FIX: (Android) flutter initialization +## 0.2.5 +* FIX: (iOS) using other plugins +## 0.2.4 +* FIX: (Android) run service background when charger not connected and screen lock (#92) +## 0.2.3 +* ADDED: Using `BGTaskScheduler` on iOS 13. See readme for configuration. +## 0.2.2 +* ADDED: `autoStart` to `IosConfiguration` +## 0.2.1 +* UPDATE README +* UPDATE: Flutter Version Constraint +## 0.2.0+1 +* UPDATE README + +## 0.2.0 +* [BREAKING]: FlutterBackgroundService.initialize renamed to FlutterBackgroundService.configure +* [BREAKING]: use FlutterBackgroundService.start to start or restart after you call stopService. +* [ADDED]: IOS Background fetch is now supported you have to enable background fetch from xcode. +## 0.1.7 + +* Fix : cannot start service on android 12 +* Fix : not started on boot completed +## 0.1.6 + +* Android 12 Compatibility Changes +## 0.1.5 + +* Rollback foreground notification importance +## 0.1.4 + +* fixes UnsatisfiedLinkError when running as foreground service with autostart #32 +## 0.1.3 + +* Fix notification not showing on android 7 and prior (Issue #26) +## 0.1.2 + +* Open app from notification (Issue #30) +## 0.1.1 + +* Fix #29 (DartVM not terminated when service stop) + +## 0.1.0 + +* Bump flutter 2 + +## 0.1.0-nullsafety.2 + +* Fix #23 + +## 0.1.0-nullsafety.1 + +* Added isServiceRunning on iOS (issue #19) + +## 0.1.0-nullsafety.0 + +* Added support to nullsafety + +## 0.0.1+18 + +* Added stopService Method(Currently Works on Android Only). + +## 0.0.1+17 + +* Add preference autoStart on Boot, default is true. + +## 0.0.1+16 + +* Set Foreground Mode to false will remove notification. BugFix #4. + +## 0.0.1+15 + +* Add ability to change Background or Foreground mode (Android Only) + +## 0.0.1+14 + +* Bugfix BootReceiver + +## 0.0.1+13 + +* Update example for iOS support. + +## 0.0.1+12 + +* Start service immediately after initialize + +## 0.0.1+11 + +* iOS + +## 0.0.1+10 + +* bug fix + +## 0.0.1+9 + +* bug fix + +## 0.0.1+8 + +* bug fix + +## 0.0.1+7 + +* Add ability to send data from UI to Service + +## 0.0.1+6 + +* Improve stability + +## 0.0.1+5 + +* Add ability to send data from service to UI + +## 0.0.1+4 + +* Update README + +## 0.0.1+3 + +* Add ability to change notification info (Android foreground service) + +## 0.0.1+2 + +* Fix android missing plugin implementation + +## 0.0.1+1 + +* Fix android build + +## 0.0.1 + +* TODO: Describe initial release. \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6f3b2ac --- /dev/null +++ b/LICENSE @@ -0,0 +1,25 @@ +Copyright 2017 The Chromium Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..3459b08 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,37 @@ +group 'id.flutter.flutter_background_service' +version '1.0' + +buildscript { + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.1.0' + } +} + +rootProject.allprojects { + repositories { + google() + jcenter() + } +} + +apply plugin: 'com.android.library' + +android { + compileSdkVersion 31 + + defaultConfig { + minSdkVersion 16 + } + lintOptions { + disable 'InvalidPackage' + } +} + +dependencies { + implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0' +} \ No newline at end of file diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..38c8d45 --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx1536M +android.enableR8=true +android.useAndroidX=true +android.enableJetifier=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..bc6a58a --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..1f1100c --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'flutter_background_service' diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..e0d0392 --- /dev/null +++ b/android/src/main/AndroidManifest.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/android/src/main/java/id/flutter/flutter_background_service/BackgroundService.java b/android/src/main/java/id/flutter/flutter_background_service/BackgroundService.java new file mode 100644 index 0000000..dbe12dd --- /dev/null +++ b/android/src/main/java/id/flutter/flutter_background_service/BackgroundService.java @@ -0,0 +1,327 @@ +package id.flutter.flutter_background_service; + +import static android.os.Build.VERSION.SDK_INT; + +import android.app.AlarmManager; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Build; +import android.os.IBinder; +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.core.app.AlarmManagerCompat; +import androidx.core.app.NotificationCompat; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.lang.UnsatisfiedLinkError; + +import io.flutter.FlutterInjector; +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.embedding.engine.dart.DartExecutor; +import io.flutter.embedding.engine.loader.FlutterLoader; +import io.flutter.plugin.common.JSONMethodCodec; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.view.FlutterCallbackInformation; + +public class BackgroundService extends Service implements MethodChannel.MethodCallHandler { + private static final String TAG = "BackgroundService"; + private FlutterEngine backgroundEngine; + private MethodChannel methodChannel; + private DartExecutor.DartCallback dartCallback; + private boolean isManuallyStopped = false; + + String notificationTitle = "短信帮手正在后台运行"; + String notificationContent = "Running"; + private static final String LOCK_NAME = BackgroundService.class.getName() + + ".Lock"; + public static volatile WakeLock lockStatic = null; // notice static + + synchronized public static PowerManager.WakeLock getLock(Context context) { + if (lockStatic == null) { + PowerManager mgr = (PowerManager) context + .getSystemService(Context.POWER_SERVICE); + lockStatic = mgr.newWakeLock(PowerManager.FULL_WAKE_LOCK, + LOCK_NAME); + lockStatic.setReferenceCounted(true); + } + return (lockStatic); + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + public static void enqueue(Context context) { + Intent intent = new Intent(context, WatchdogReceiver.class); + AlarmManager manager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + + int flags = PendingIntent.FLAG_UPDATE_CURRENT; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + flags |= PendingIntent.FLAG_MUTABLE; + } + + PendingIntent pIntent = PendingIntent.getBroadcast(context, 111, intent, flags); + AlarmManagerCompat.setAndAllowWhileIdle(manager, AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 5000, pIntent); + } + + public void setAutoStartOnBootMode(boolean value) { + SharedPreferences pref = getSharedPreferences("id.flutter.background_service", MODE_PRIVATE); + pref.edit().putBoolean("auto_start_on_boot", value).apply(); + } + + public static boolean isAutoStartOnBootMode(Context context) { + SharedPreferences pref = context.getSharedPreferences("id.flutter.background_service", MODE_PRIVATE); + return pref.getBoolean("auto_start_on_boot", true); + } + + public void setForegroundServiceMode(boolean value) { + SharedPreferences pref = getSharedPreferences("id.flutter.background_service", MODE_PRIVATE); + pref.edit().putBoolean("is_foreground", value).apply(); + } + + public static boolean isForegroundService(Context context) { + SharedPreferences pref = context.getSharedPreferences("id.flutter.background_service", MODE_PRIVATE); + return pref.getBoolean("is_foreground", true); + } + + public void setManuallyStopped(boolean value) { + SharedPreferences pref = getSharedPreferences("id.flutter.background_service", MODE_PRIVATE); + pref.edit().putBoolean("is_manually_stopped", value).apply(); + } + + public static boolean isManuallyStopped(Context context) { + SharedPreferences pref = context.getSharedPreferences("id.flutter.background_service", MODE_PRIVATE); + return pref.getBoolean("is_manually_stopped", false); + } + + @Override + public void onCreate() { + super.onCreate(); + createNotificationChannel(); + notificationContent = "点击即可查看更多选项。"; + updateNotificationInfo(); + } + + @Override + public void onDestroy() { + if (!isManuallyStopped) { + enqueue(this); + } else { + setManuallyStopped(true); + } + stopForeground(true); + isRunning.set(false); + + if (backgroundEngine != null) { + backgroundEngine.getServiceControlSurface().detachFromService(); + backgroundEngine.destroy(); + backgroundEngine = null; + } + + methodChannel = null; + dartCallback = null; + super.onDestroy(); + } + + private void createNotificationChannel() { + if (SDK_INT >= Build.VERSION_CODES.O) { + CharSequence name = "Background Service"; + String description = "Executing process in background"; + + int importance = NotificationManager.IMPORTANCE_LOW; + NotificationChannel channel = new NotificationChannel("FOREGROUND_DEFAULT", name, importance); + channel.setDescription(description); + + NotificationManager notificationManager = getSystemService(NotificationManager.class); + notificationManager.createNotificationChannel(channel); + } + } + + protected void updateNotificationInfo() { + if (isForegroundService(this)) { + + String packageName = getApplicationContext().getPackageName(); + Intent i = getPackageManager().getLaunchIntentForPackage(packageName); + + int flags = PendingIntent.FLAG_CANCEL_CURRENT; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + flags |= PendingIntent.FLAG_MUTABLE; + } + + PendingIntent pi = PendingIntent.getActivity(BackgroundService.this, 99778, i, flags); + + NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(this, "FOREGROUND_DEFAULT") + .setSmallIcon(R.drawable.ic_bg_service_small) + .setAutoCancel(true) + .setOngoing(true) + .setContentTitle(notificationTitle) + .setContentText(notificationContent) + .setContentIntent(pi); + + startForeground(99778, mBuilder.build()); + } + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + setManuallyStopped(false); + enqueue(this); + runService(); + + return START_STICKY; + } + + AtomicBoolean isRunning = new AtomicBoolean(false); + + private void runService() { + try { + Log.d(TAG, "runService"); + if (isRunning.get() || (backgroundEngine != null && !backgroundEngine.getDartExecutor().isExecutingDart())) + return; + + if (lockStatic == null){ + getLock(getApplicationContext()).acquire(10*60*1000L /*10 minutes*/); + } + + updateNotificationInfo(); + + SharedPreferences pref = getSharedPreferences("id.flutter.background_service", MODE_PRIVATE); + long entrypointHandle = pref.getLong("entrypoint_handle", 0); + + FlutterLoader flutterLoader = FlutterInjector.instance().flutterLoader(); + // initialize flutter if it's not initialized yet + if (!flutterLoader.initialized()) { + flutterLoader.startInitialization(getApplicationContext()); + } + + flutterLoader.ensureInitializationComplete(getApplicationContext(), null); + FlutterCallbackInformation callback = FlutterCallbackInformation.lookupCallbackInformation(entrypointHandle); + if (callback == null) { + Log.e(TAG, "callback handle not found"); + return; + } + + isRunning.set(true); + backgroundEngine = new FlutterEngine(this); + backgroundEngine.getServiceControlSurface().attachToService(BackgroundService.this, null, isForegroundService(this)); + + methodChannel = new MethodChannel(backgroundEngine.getDartExecutor().getBinaryMessenger(), "id.flutter/background_service_android_bg", JSONMethodCodec.INSTANCE); + methodChannel.setMethodCallHandler(this); + + dartCallback = new DartExecutor.DartCallback(getAssets(), flutterLoader.findAppBundlePath(), callback); + backgroundEngine.getDartExecutor().executeDartCallback(dartCallback); + } catch (UnsatisfiedLinkError e) { + notificationContent = "Error " + e.getMessage(); + updateNotificationInfo(); + + Log.w(TAG, "UnsatisfiedLinkError: After a reboot this may happen for a short period and it is ok to ignore then!" + e.getMessage()); + } + } + + public void receiveData(JSONObject data) { + if (methodChannel != null) { + try { + methodChannel.invokeMethod("onReceiveData", data); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + String method = call.method; + + try { + if (method.equalsIgnoreCase("getHandler")) { + SharedPreferences pref = getSharedPreferences("id.flutter.background_service", MODE_PRIVATE); + long backgroundHandle = pref.getLong("background_handle", 0); + result.success(backgroundHandle); + + if (lockStatic != null) { + lockStatic.release(); + lockStatic = null; + } + return; + } + + if (method.equalsIgnoreCase("setNotificationInfo")) { + JSONObject arg = (JSONObject) call.arguments; + if (arg.has("title")) { + notificationTitle = arg.getString("title"); + notificationContent = arg.getString("content"); + updateNotificationInfo(); + result.success(true); + } + return; + } + + if (method.equalsIgnoreCase("setAutoStartOnBootMode")) { + JSONObject arg = (JSONObject) call.arguments; + boolean value = arg.getBoolean("value"); + setAutoStartOnBootMode(value); + result.success(true); + return; + } + + if (method.equalsIgnoreCase("setForegroundMode")) { + JSONObject arg = (JSONObject) call.arguments; + boolean value = arg.getBoolean("value"); + setForegroundServiceMode(value); + if (value) { + updateNotificationInfo(); + } else { + stopForeground(true); + } + + result.success(true); + return; + } + + if (method.equalsIgnoreCase("stopService")) { + isManuallyStopped = true; + Intent intent = new Intent(this, WatchdogReceiver.class); + + int flags = PendingIntent.FLAG_CANCEL_CURRENT; + if (SDK_INT >= Build.VERSION_CODES.S) { + flags |= PendingIntent.FLAG_MUTABLE; + } + + PendingIntent pi = PendingIntent.getBroadcast(getApplicationContext(), 111, intent, flags); + + AlarmManager alarmManager = (AlarmManager) getSystemService(ALARM_SERVICE); + alarmManager.cancel(pi); + stopSelf(); + result.success(true); + return; + } + + if (method.equalsIgnoreCase("sendData")) { + LocalBroadcastManager manager = LocalBroadcastManager.getInstance(this); + Intent intent = new Intent("id.flutter/background_service"); + intent.putExtra("data", call.arguments.toString()); + manager.sendBroadcast(intent); + result.success(true); + return; + } + } catch (JSONException e) { + Log.e(TAG, e.getMessage()); + e.printStackTrace(); + } + + result.notImplemented(); + } +} diff --git a/android/src/main/java/id/flutter/flutter_background_service/BootReceiver.java b/android/src/main/java/id/flutter/flutter_background_service/BootReceiver.java new file mode 100644 index 0000000..7059485 --- /dev/null +++ b/android/src/main/java/id/flutter/flutter_background_service/BootReceiver.java @@ -0,0 +1,30 @@ +package id.flutter.flutter_background_service; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; + +import androidx.core.content.ContextCompat; + +import static android.content.Context.MODE_PRIVATE; + + +public class BootReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + SharedPreferences pref = context.getSharedPreferences("id.flutter.background_service", MODE_PRIVATE); + boolean autoStart = pref.getBoolean("auto_start_on_boot",true); + if(autoStart) { + if (BackgroundService.lockStatic == null){ + BackgroundService.getLock(context).acquire(10*60*1000L /*10 minutes*/); + } + + if (BackgroundService.isForegroundService(context)) { + ContextCompat.startForegroundService(context, new Intent(context, BackgroundService.class)); + } else { + context.startService(new Intent(context, BackgroundService.class)); + } + } + } +} diff --git a/android/src/main/java/id/flutter/flutter_background_service/FlutterBackgroundServicePlugin.java b/android/src/main/java/id/flutter/flutter_background_service/FlutterBackgroundServicePlugin.java new file mode 100644 index 0000000..cb8f4bc --- /dev/null +++ b/android/src/main/java/id/flutter/flutter_background_service/FlutterBackgroundServicePlugin.java @@ -0,0 +1,183 @@ +package id.flutter.flutter_background_service; + +import static android.content.Context.MODE_PRIVATE; + +import android.app.ActivityManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; + +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.embedding.engine.plugins.service.ServiceAware; +import io.flutter.embedding.engine.plugins.service.ServicePluginBinding; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.MethodChannel.MethodCallHandler; +import io.flutter.plugin.common.MethodChannel.Result; +import io.flutter.plugin.common.PluginRegistry.Registrar; +import io.flutter.plugin.common.JSONMethodCodec; + +/** FlutterBackgroundServicePlugin */ +public class FlutterBackgroundServicePlugin extends BroadcastReceiver implements FlutterPlugin, MethodCallHandler, ServiceAware { + private static final String TAG = "BackgroundServicePlugin"; + private static final List _instances = new ArrayList<>(); + + public FlutterBackgroundServicePlugin() { + _instances.add(this); + } + + private MethodChannel channel; + private Context context; + private BackgroundService service; + + @Override + public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) { + this.context = flutterPluginBinding.getApplicationContext(); + LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(this.context); + localBroadcastManager.registerReceiver(this, new IntentFilter("id.flutter/background_service")); + + channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "id.flutter/background_service_android", JSONMethodCodec.INSTANCE); + channel.setMethodCallHandler(this); + } + + public static void registerWith(Registrar registrar) { + LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(registrar.context()); + final FlutterBackgroundServicePlugin plugin = new FlutterBackgroundServicePlugin(); + localBroadcastManager.registerReceiver(plugin, new IntentFilter("id.flutter/background_service")); + + final MethodChannel channel = new MethodChannel(registrar.messenger(), "id.flutter/background_service_android", JSONMethodCodec.INSTANCE); + channel.setMethodCallHandler(plugin); + plugin.channel = channel; + } + + private static void configure(Context context, long entrypointHandle, long backgroundHandle, boolean isForeground, boolean autoStartOnBoot) { + SharedPreferences pref = context.getSharedPreferences("id.flutter.background_service", MODE_PRIVATE); + pref.edit() + .putLong("entrypoint_handle", entrypointHandle) + .putLong("background_handle", backgroundHandle) + .putBoolean("is_foreground", isForeground) + .putBoolean("auto_start_on_boot", autoStartOnBoot) + .apply(); + } + + private void start() { + BackgroundService.enqueue(context); + boolean isForeground = BackgroundService.isForegroundService(context); + Intent intent = new Intent(context, BackgroundService.class); + if (isForeground) { + ContextCompat.startForegroundService(context, intent); + } else { + context.startService(intent); + } + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { + String method = call.method; + JSONObject arg = (JSONObject) call.arguments; + + try { + if ("configure".equals(method)) { + long entrypointHandle = arg.getLong("entrypoint_handle"); + long backgroundHandle = arg.getLong("background_handle"); + boolean isForeground = arg.getBoolean("is_foreground_mode"); + boolean autoStartOnBoot = arg.getBoolean("auto_start_on_boot"); + + configure(context, entrypointHandle, backgroundHandle, isForeground, autoStartOnBoot); + if (autoStartOnBoot) { + start(); + } + + result.success(true); + return; + } + + if ("start".equals(method)) { + start(); + result.success(true); + return; + } + + if (method.equalsIgnoreCase("sendData")) { + for (FlutterBackgroundServicePlugin plugin : _instances) { + if (plugin.service != null) { + plugin.service.receiveData((JSONObject) call.arguments); + break; + } + } + + result.success(true); + return; + } + + if (method.equalsIgnoreCase("isServiceRunning")) { + ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) { + if (BackgroundService.class.getName().equals(service.service.getClassName())) { + result.success(true); + return; + } + } + result.success(false); + return; + } + + result.notImplemented(); + } catch (Exception e) { + result.error("100", "Failed read arguments", null); + } + } + + @Override + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { + channel.setMethodCallHandler(null); + + LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(this.context); + localBroadcastManager.unregisterReceiver(this); + } + + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction() == null) return; + + if (intent.getAction().equalsIgnoreCase("id.flutter/background_service")) { + String data = intent.getStringExtra("data"); + try { + JSONObject jData = new JSONObject(data); + if (channel != null) { + channel.invokeMethod("onReceiveData", jData); + } + } catch (JSONException e) { + e.printStackTrace(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + @Override + public void onAttachedToService(@NonNull ServicePluginBinding binding) { + Log.d(TAG, "onAttachedToService"); + + this.service = (BackgroundService) binding.getService(); + } + + @Override + public void onDetachedFromService() { + this.service = null; + Log.d(TAG, "onDetachedFromService"); + } +} diff --git a/android/src/main/java/id/flutter/flutter_background_service/WatchdogReceiver.java b/android/src/main/java/id/flutter/flutter_background_service/WatchdogReceiver.java new file mode 100644 index 0000000..043558a --- /dev/null +++ b/android/src/main/java/id/flutter/flutter_background_service/WatchdogReceiver.java @@ -0,0 +1,20 @@ +package id.flutter.flutter_background_service; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import androidx.core.content.ContextCompat; + +public class WatchdogReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if(!BackgroundService.isManuallyStopped(context)) { + if (BackgroundService.isForegroundService(context)) { + ContextCompat.startForegroundService(context, new Intent(context, BackgroundService.class)); + } else { + context.startService(new Intent(context, BackgroundService.class)); + } + } + } +} diff --git a/android/src/main/res/drawable-anydpi-v24/ic_bg_service_small.xml b/android/src/main/res/drawable-anydpi-v24/ic_bg_service_small.xml new file mode 100644 index 0000000..39d15ef --- /dev/null +++ b/android/src/main/res/drawable-anydpi-v24/ic_bg_service_small.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/android/src/main/res/drawable-hdpi/ic_bg_service_small.png b/android/src/main/res/drawable-hdpi/ic_bg_service_small.png new file mode 100644 index 0000000..4f4a5c8 Binary files /dev/null and b/android/src/main/res/drawable-hdpi/ic_bg_service_small.png differ diff --git a/android/src/main/res/drawable-mdpi/ic_bg_service_small.png b/android/src/main/res/drawable-mdpi/ic_bg_service_small.png new file mode 100644 index 0000000..a76c638 Binary files /dev/null and b/android/src/main/res/drawable-mdpi/ic_bg_service_small.png differ diff --git a/android/src/main/res/drawable-xhdpi/ic_bg_service_small.png b/android/src/main/res/drawable-xhdpi/ic_bg_service_small.png new file mode 100644 index 0000000..8e91e9c Binary files /dev/null and b/android/src/main/res/drawable-xhdpi/ic_bg_service_small.png differ diff --git a/android/src/main/res/drawable-xxhdpi/ic_bg_service_small.png b/android/src/main/res/drawable-xxhdpi/ic_bg_service_small.png new file mode 100644 index 0000000..6c378c6 Binary files /dev/null and b/android/src/main/res/drawable-xxhdpi/ic_bg_service_small.png differ diff --git a/flutter_background_service.iml b/flutter_background_service.iml new file mode 100644 index 0000000..429df7d --- /dev/null +++ b/flutter_background_service.iml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lib/flutter_background_service_android.dart b/lib/flutter_background_service_android.dart new file mode 100644 index 0000000..92d5a47 --- /dev/null +++ b/lib/flutter_background_service_android.dart @@ -0,0 +1,186 @@ +import 'dart:async'; +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_background_service_platform_interface/flutter_background_service_platform_interface.dart'; + +Future _entrypoint() async { + WidgetsFlutterBinding.ensureInitialized(); + final service = AndroidServiceInstance._(); + final int handle = await service._getHandler(); + final callbackHandle = CallbackHandle.fromRawHandle(handle); + final onStart = PluginUtilities.getCallbackFromHandle(callbackHandle); + if (onStart != null) { + onStart(service); + } +} + +class FlutterBackgroundServiceAndroid extends FlutterBackgroundServicePlatform { + /// Registers this class as the default instance of [FlutterBackgroundServicePlatform]. + static void registerWith() { + FlutterBackgroundServicePlatform.instance = + FlutterBackgroundServiceAndroid(); + } + + static const MethodChannel _channel = const MethodChannel( + 'id.flutter/background_service_android', + JSONMethodCodec(), + ); + + Future _handle(MethodCall call) async { + switch (call.method) { + case "onReceiveData": + _controller.sink.add(call.arguments); + break; + default: + } + + return true; + } + + Future start() async { + final result = await _channel.invokeMethod('start'); + return result ?? false; + } + + Future configure({ + required IosConfiguration iosConfiguration, + required AndroidConfiguration androidConfiguration, + }) async { + _channel.setMethodCallHandler(_handle); + + final CallbackHandle? entryPointHandle = + PluginUtilities.getCallbackHandle(_entrypoint); + + final CallbackHandle? handle = + PluginUtilities.getCallbackHandle(androidConfiguration.onStart); + + if (entryPointHandle == null || handle == null) { + return false; + } + + final result = await _channel.invokeMethod( + "configure", + { + "entrypoint_handle": entryPointHandle.toRawHandle(), + "background_handle": handle.toRawHandle(), + "is_foreground_mode": androidConfiguration.isForegroundMode, + "auto_start_on_boot": androidConfiguration.autoStart, + }, + ); + + return result ?? false; + } + + Future isServiceRunning() async { + var result = await _channel.invokeMethod("isServiceRunning"); + return result ?? false; + } + + final _controller = StreamController.broadcast(sync: true); + + void dispose() { + _controller.close(); + } + + @override + void invoke(String method, [Map? args]) { + _channel.invokeMethod("sendData", { + 'method': method, + 'args': args, + }); + } + + @override + Stream?> on(String method) { + return _controller.stream.transform( + StreamTransformer.fromHandlers( + handleData: (data, sink) { + if (data['method'] == method) { + sink.add(data['args']); + } + }, + ), + ); + } +} + +class AndroidServiceInstance extends ServiceInstance { + static const MethodChannel _channel = const MethodChannel( + 'id.flutter/background_service_android_bg', + JSONMethodCodec(), + ); + + AndroidServiceInstance._() { + _channel.setMethodCallHandler(_handleMethodCall); + } + + final _controller = StreamController.broadcast(sync: true); + Future _handleMethodCall(MethodCall call) async { + switch (call.method) { + case "onReceiveData": + _controller.sink.add(call.arguments); + break; + default: + } + } + + @override + void invoke(String method, [Map? args]) { + _channel.invokeMethod('sendData', { + 'method': method, + 'args': args, + }); + } + + @override + Future stopSelf() async { + await _channel.invokeMethod("stopService"); + } + + @override + Stream?> on(String method) { + return _controller.stream.transform( + StreamTransformer.fromHandlers( + handleData: (data, sink) { + if (data['method'] == method) { + sink.add(data['args']); + } + }, + ), + ); + } + + Future setForegroundNotificationInfo({ + required String title, + required String content, + }) async { + await _channel.invokeMethod("setNotificationInfo", { + "title": title, + "content": content, + }); + } + + Future setAsForegroundService() async { + await _channel.invokeMethod("setForegroundMode", { + 'value': true, + }); + } + + Future setAsBackgroundService() async { + await _channel.invokeMethod("setForegroundMode", { + 'value': false, + }); + } + + Future _getHandler() async { + return await _channel.invokeMethod('getHandler'); + } + + Future setAutoStartOnBootMode(bool value) async { + await _channel.invokeMethod("setAutoStartOnBootMode", { + "value": value, + }); + } +} diff --git a/melos_flutter_background_service_android.iml b/melos_flutter_background_service_android.iml new file mode 100644 index 0000000..9fc8ce7 --- /dev/null +++ b/melos_flutter_background_service_android.iml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..8785694 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,63 @@ +name: flutter_background_service_android +description: A flutter plugin for executing dart code continously even application closed. +version: 2.0.3 +repository: https://github.com/ekasetiawans/flutter_background_service + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" + +dependencies: + flutter: + sdk: flutter + + flutter_background_service_platform_interface: ^2.0.0 +dev_dependencies: + flutter_test: + sdk: flutter + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + # This section identifies this Flutter project as a plugin project. + # The 'pluginClass' and Android 'package' identifiers should not ordinarily + # be modified. They are used by the tooling to maintain consistency when + # adding or updating assets for this project. + plugin: + implements: flutter_background_service + platforms: + android: + package: id.flutter.flutter_background_service + pluginClass: FlutterBackgroundServicePlugin + dartPluginClass: FlutterBackgroundServiceAndroid + # To add assets to your plugin package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + # To add custom fonts to your plugin package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/test/flutter_background_service_test.dart b/test/flutter_background_service_test.dart new file mode 100644 index 0000000..ab73b3a --- /dev/null +++ b/test/flutter_background_service_test.dart @@ -0,0 +1 @@ +void main() {}