Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
Expand Down Expand Up @@ -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'
Expand Down
15 changes: 15 additions & 0 deletions android/app/src/main/java/com/example/background/App.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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) {}

}
53 changes: 53 additions & 0 deletions android/app/src/main/java/com/example/background/MainActivity.java
Original file line number Diff line number Diff line change
@@ -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");
}
}
);
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}