diff --git a/android/app/build.gradle b/android/app/build.gradle index 144f580..f4e46cf 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -22,13 +22,13 @@ if (flutterVersionName == null) { } apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android' // comment this line if using java apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" -apply plugin: 'kotlin-android-extensions' - +apply plugin: 'kotlin-android-extensions' // comment this line if using java android { compileSdkVersion 28 + // comment `sourceSets` block if using java sourceSets { main.java.srcDirs += 'src/main/kotlin' } @@ -61,7 +61,7 @@ flutter { } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" // comment this line if using java implementation "androidx.appcompat:appcompat:1.2.0" testImplementation 'junit:junit:4.13' androidTestImplementation 'androidx.test:runner:1.3.0' diff --git a/android/app/src/main/java/com/example/background/App.java b/android/app/src/main/java/com/example/background/App.java new file mode 100644 index 0000000..278c29b --- /dev/null +++ b/android/app/src/main/java/com/example/background/App.java @@ -0,0 +1,15 @@ +package com.example.background; + +import android.app.Application; + + +public class App extends Application { + + @Override + public void onCreate() { + super.onCreate(); + + LifecycleDetector lifecycleDetector = LifecycleDetector.getInstance(); + registerActivityLifecycleCallbacks(lifecycleDetector); + } +} diff --git a/android/app/src/main/java/com/example/background/BackgroundService.java b/android/app/src/main/java/com/example/background/BackgroundService.java new file mode 100644 index 0000000..932221d --- /dev/null +++ b/android/app/src/main/java/com/example/background/BackgroundService.java @@ -0,0 +1,122 @@ +package com.example.background; + +import android.app.Service; +import android.app.Notification; +import android.app.NotificationChannel; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.AssetManager; +import android.os.Build; +import android.os.IBinder; + +import android.util.Log; +import java.lang.InterruptedException; + +import androidx.annotation.Nullable; +import androidx.core.app.NotificationCompat; +import androidx.core.content.ContextCompat; + +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.embedding.engine.dart.DartExecutor; +import io.flutter.view.FlutterCallbackInformation; +import io.flutter.view.FlutterMain; +import io.flutter.plugins.GeneratedPluginRegistrant; + + +public class BackgroundService extends Service implements LifecycleDetector.Listener { + + private FlutterEngine flutterEngine; + + private static String SHARED_PREFERENCES_NAME = "com.exmaple.background.BackgroundService"; + private static String KEY_CALLBACK_RAW_HANDLE = "callbackRawHandle"; + + private LifecycleDetector instance; + + @Override + public void onCreate() { + super.onCreate(); + Notification notification = Notifications.buildForegroundNotification(BackgroundService.this); + startForeground(Notifications.NOTIFICATION_ID_BACKGROUND_SERVICE, notification); + LifecycleDetector instance = LifecycleDetector.getInstance(); + instance.listener = BackgroundService.this; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (intent != null) { + Long callbackRawHandle = intent.getLongExtra(KEY_CALLBACK_RAW_HANDLE, -1); + if (callbackRawHandle != null) { + if (callbackRawHandle != -1L) { + setCallbackRawHandle(callbackRawHandle); + } + } + } + if (!LifecycleDetector.getInstance().getIsActivityRunning()) { + startFlutterNativeView(); + } + return START_STICKY; + } + + @Override + public void onDestroy(){ + super.onDestroy(); + instance.listener = null; + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public void onFlutterActivityCreated() { + stopFlutterNativeView(); + } + + @Override + public void onFlutterActivityDestroyed() { + startFlutterNativeView(); + } + + private void startFlutterNativeView() { + if (flutterEngine != null) return; + + Log.i("BackgroundService", "Starting FlutterEngine"); + Long callbackRawHandle = getCallbackRawHandle(); + if (callbackRawHandle != null) { + FlutterCallbackInformation callbackInformation = + FlutterCallbackInformation.lookupCallbackInformation(callbackRawHandle); + + flutterEngine = new FlutterEngine(this); + DartExecutor executor = flutterEngine.getDartExecutor(); + String appBundlePath = FlutterMain.findAppBundlePath(); + AssetManager assets = this.getAssets(); + + DartExecutor.DartCallback dartCallback = new DartExecutor.DartCallback(assets, appBundlePath, callbackInformation); + executor.executeDartCallback(dartCallback); + } + } + + private void stopFlutterNativeView() { + Log.i("BackgroundService", "Stopping FlutterEngine"); + if (flutterEngine != null) flutterEngine.destroy(); + flutterEngine = null; + } + + private Long getCallbackRawHandle() { + Long callbackRawHandle = getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE).getLong(KEY_CALLBACK_RAW_HANDLE, -1); + return (callbackRawHandle != -1L) ? callbackRawHandle : null; + } + + private void setCallbackRawHandle(Long handle) { + SharedPreferences prefs = getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE); + prefs.edit().putLong(KEY_CALLBACK_RAW_HANDLE, handle).commit(); + } + + public static void startService(Context context, Long callbackRawHandle) { + Intent intent = new Intent(context, BackgroundService.class); + intent.putExtra(KEY_CALLBACK_RAW_HANDLE, callbackRawHandle); + ContextCompat.startForegroundService(context, intent); + } +} diff --git a/android/app/src/main/java/com/example/background/LifecycleDetector.java b/android/app/src/main/java/com/example/background/LifecycleDetector.java new file mode 100644 index 0000000..5a94fbb --- /dev/null +++ b/android/app/src/main/java/com/example/background/LifecycleDetector.java @@ -0,0 +1,73 @@ +package com.example.background; + +import android.app.Activity; +import android.app.Application; +import android.os.Bundle; + + +public class LifecycleDetector implements Application.ActivityLifecycleCallbacks { + + private static LifecycleDetector instance = null; + + private LifecycleDetector(){} + + private synchronized static void createInstance() { + if (instance == null) { + instance = new LifecycleDetector(); + } + } + + public static LifecycleDetector getInstance() { + if (instance == null) createInstance(); + return instance; + } + + private boolean isActivityRunning = false; + + public void setIsActivityRunning(boolean value) { + isActivityRunning = value; + } + + public boolean getIsActivityRunning() { + return isActivityRunning; + } + + Listener listener; + + public static interface Listener { + void onFlutterActivityCreated(); + void onFlutterActivityDestroyed(); + } + + @Override + public void onActivityCreated(Activity activity, Bundle savedInstanceState) { + if (activity instanceof MainActivity) { + isActivityRunning = true; + if (listener != null) listener.onFlutterActivityCreated(); + } + } + + @Override + public void onActivityDestroyed(Activity activity) { + if (activity instanceof MainActivity) { + isActivityRunning = false; + if (listener != null) listener.onFlutterActivityDestroyed(); + } + } + + @Override + public void onActivityStarted(Activity activity) {} + + @Override + public void onActivityResumed(Activity activity) {} + + @Override + public void onActivityPaused(Activity activity) {} + + @Override + public void onActivityStopped(Activity activity) {} + + @Override + public void onActivitySaveInstanceState(Activity activity, Bundle outState) {} + +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/background/MainActivity.java b/android/app/src/main/java/com/example/background/MainActivity.java new file mode 100644 index 0000000..42dff26 --- /dev/null +++ b/android/app/src/main/java/com/example/background/MainActivity.java @@ -0,0 +1,53 @@ +package com.example.background; + +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import androidx.annotation.NonNull; +import android.util.Log; + +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugins.GeneratedPluginRegistrant; + +public class MainActivity extends FlutterActivity { + + private Intent forService; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Notifications.createNotificationChannels(this); + } + + @Override + public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { + GeneratedPluginRegistrant.registerWith(flutterEngine); + + new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), "com.example/background_service") + .setMethodCallHandler( + (call, result) -> { + if (call.method.equals("startService")) { + Long callbackRawHandle = call.arguments(); + BackgroundService background_service = new BackgroundService(); + background_service.startService(MainActivity.this, callbackRawHandle); + result.success(null); + } else { + result.notImplemented(); + } + } + ); + + new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), "com.example/app_retain") + .setMethodCallHandler( + (call, result) -> { + if (call.method.equals("sendToBackground")) { + moveTaskToBack(true); + result.success("Moved task to back"); + } + } + ); + } +} diff --git a/android/app/src/main/java/com/example/background/Notifications.java b/android/app/src/main/java/com/example/background/Notifications.java new file mode 100644 index 0000000..adc9280 --- /dev/null +++ b/android/app/src/main/java/com/example/background/Notifications.java @@ -0,0 +1,37 @@ +package com.example.background; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.content.Context; +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; +import android.os.Build; +import androidx.core.app.NotificationCompat; + +public class Notifications { + final static int NOTIFICATION_ID_BACKGROUND_SERVICE = 1; + + final static String CHANNEL_ID_BACKGROUND_SERVICE = "background_service"; + + public static void createNotificationChannels(Context context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = new NotificationChannel( + CHANNEL_ID_BACKGROUND_SERVICE, + "Background Service", + NotificationManager.IMPORTANCE_DEFAULT + ); + NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + manager.createNotificationChannel(channel); + } + } + + public static Notification buildForegroundNotification(Context context) { + return new NotificationCompat.Builder(context, CHANNEL_ID_BACKGROUND_SERVICE) + .setSmallIcon(R.mipmap.ic_launcher) + .setContentTitle("Background Service") + .setContentText("Keeps app process on foreground.") + .build(); + } +}