Skip to content

Commit

Permalink
[example][android] Add foreground service example
Browse files Browse the repository at this point in the history
  • Loading branch information
littleGnAl committed Aug 10, 2023
1 parent 49e55bf commit 32af54a
Show file tree
Hide file tree
Showing 7 changed files with 277 additions and 15 deletions.
11 changes: 10 additions & 1 deletion example/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="io.agora.agora_rtc_ng_example">

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

<application
android:label="agora_rtc_ng_example"
android:name="${applicationName}"
android:name=".MyApplication"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
Expand Down Expand Up @@ -30,5 +33,11 @@
<meta-data
android:name="flutterEmbedding"
android:value="2" />

<service
android:name=".ExampleService"
android:exported="false"
android:foregroundServiceType="mediaPlayback|microphone"
android:stopWithTask="false" />
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package io.agora.agora_rtc_ng_example

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.os.Binder
import android.os.Build
import android.os.Bundle
import android.os.IBinder
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat

class ExampleService: Service() {

private val binder: Binder by lazy {
Binder()
}

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
return if (intent?.action == ACTION_START_FOREGROUND_SERVICE) {
startForegroundService()
START_REDELIVER_INTENT
} else {
stopForegroundService()
START_NOT_STICKY
}
}

override fun onTaskRemoved(rootIntent: Intent?) {
super.onTaskRemoved(rootIntent)
// If we receive the `onTaskRemoved`, we consider that the user want to close the APP, so
// destroy the `FlutterEngine` here.
(applicationContext as MyApplication).destroyFlutterEngine()

stopForegroundService()
}

override fun onBind(intent: Intent): IBinder? {
return binder
}

private fun createNotificationChannel(
context: Context,
channelId: String,
name: String,
des: String
) {
val notificationManager =
ContextCompat.getSystemService(
context,
NotificationManager::class.java
) as NotificationManager

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (notificationManager.getNotificationChannel(channelId) == null) {
val notificationChannel = NotificationChannel(
channelId,
name,
NotificationManager.IMPORTANCE_DEFAULT
)
notificationChannel.setSound(null, null)
notificationChannel.enableLights(false)
notificationChannel.enableVibration(false)
notificationChannel.description = des
notificationManager.createNotificationChannel(notificationChannel)
}
}
}

private fun startForegroundService() {
createNotificationChannel(
applicationContext,
"MyChannelId",
"agora_rtc_engine_example",
"agora_rtc_engine_example running"
)

val notificationBuilder =
NotificationCompat
.Builder(this, "MyChannelId")
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle("agora_rtc_engine_example")
.setContentText("agora_rtc_engine_example running")
.setWhen(System.currentTimeMillis())
.setPriority(NotificationCompat.PRIORITY_LOW)
.setContentIntent(getContentIntent())
.setShowWhen(false)

startForeground(NOTIFICATION_ID_DAEMON_SERVICE, notificationBuilder.build())
}

private fun getContentIntent(): PendingIntent? {
val intent = Intent(this, MainActivity::class.java).apply {
action = "NOTIFICATION_OPEN"
}
return PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_IMMUTABLE
)
}

private fun stopForegroundService() {
stopForeground(true)
stopSelf()
}

companion object {
const val ACTION_START_FOREGROUND_SERVICE = "ACTION_START_FOREGROUND_SERVICE"
private const val ACTION_STOP_FOREGROUND_SERVICE = "ACTION_STOP_FOREGROUND_SERVICE"

const val NOTIFICATION_ID_DAEMON_SERVICE = 123

private var isServiceRunning = false

fun startDaemonService(context: Context) {
if (isServiceRunning) {
return
}
try {
val intent = Intent(context, ExampleService::class.java)
intent.action = ACTION_START_FOREGROUND_SERVICE


val bundle = Bundle()
intent.putExtras(bundle)

// Crash: startForegroundService must call start startForeground
// This crash usually happens when background global configuration changes(like: Debug DisplayCut configuration changes)
context.startService(intent)
isServiceRunning = true
} catch (e: Exception) {
e.printStackTrace()
}
}

fun stopDaemonService(context: Context) {
if (!isServiceRunning) {
return
}
try {
val intent = Intent(context, ExampleService::class.java)
intent.action = ACTION_STOP_FOREGROUND_SERVICE

// Crash: startForegroundService must call start startForeground
// This crash usually happens when background global configuration changes(like: Debug DisplayCut configuration changes)
context.startService(intent)
isServiceRunning = false
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,41 @@
package io.agora.agora_rtc_ng_example

import android.content.Context
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel

class MainActivity: FlutterActivity() {
private lateinit var methodChannel: MethodChannel

override fun provideFlutterEngine(context: Context): FlutterEngine? {
return (context.applicationContext as MyApplication).getFlutterEngine()
}

override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)

methodChannel = MethodChannel(flutterEngine.dartExecutor, "agora_rtc_ng_example/foreground_service")
methodChannel.setMethodCallHandler { call, result ->
when (call.method) {
"start_foreground_service" -> {
ExampleService.startDaemonService(MainActivity@this.applicationContext)
result.success(true)
}
"stop_foreground_service" -> {
ExampleService.stopDaemonService(MainActivity@this.applicationContext)
result.success(true)
}
else -> {
result.notImplemented()
}
}
}
}

override fun detachFromFlutterEngine() {
super.detachFromFlutterEngine()

methodChannel.setMethodCallHandler(null)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.agora.agora_rtc_ng_example

import io.flutter.app.FlutterApplication
import io.flutter.embedding.engine.FlutterEngine

class MyApplication : FlutterApplication() {
private var flutterEngine: FlutterEngine? = null

fun getFlutterEngine(): FlutterEngine {
return flutterEngine ?: FlutterEngine(this).also { flutterEngine = it }
}

fun destroyFlutterEngine() {
flutterEngine?.destroy()
flutterEngine = null
}
}
2 changes: 1 addition & 1 deletion example/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,6 @@ subprojects {
project.evaluationDependsOn(':app')
}

task clean(type: Delete) {
tasks.register("clean", Delete) {
delete rootProject.buildDir
}
35 changes: 35 additions & 0 deletions example/lib/components/android_foreground_service_widget.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

class AndroidForegroundServiceWidget extends StatefulWidget {
const AndroidForegroundServiceWidget({Key? key, required this.child}) : super(key: key);

final Widget child;

@override
State<AndroidForegroundServiceWidget> createState() => _AndroidForegroundServiceWidgetState();
}

class _AndroidForegroundServiceWidgetState extends State<AndroidForegroundServiceWidget> {

final MethodChannel _channel = const MethodChannel('agora_rtc_ng_example/foreground_service');

@override
void initState() {
super.initState();

_channel.invokeMethod('start_foreground_service');
}

@override
void dispose() {
_channel.invokeMethod('stop_foreground_service');
super.dispose();
}


@override
Widget build(BuildContext context) {
return widget.child;
}
}
34 changes: 21 additions & 13 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import 'dart:io';

import 'package:agora_rtc_engine_example/components/android_foreground_service_widget.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
Expand Down Expand Up @@ -87,19 +90,24 @@ class _MyAppState extends State<MyApp> {
)
: ListTile(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => Scaffold(
appBar: AppBar(
title: Text(
_data[index]['name'] as String),
// ignore: prefer_const_literals_to_create_immutables
actions: [const LogActionWidget()],
),
body:
_data[index]['widget'] as Widget?,
)));
Navigator.push(context,
MaterialPageRoute(builder: (context) {
Widget widget = Scaffold(
appBar: AppBar(
title: Text(_data[index]['name'] as String),
// ignore: prefer_const_literals_to_create_immutables
actions: [const LogActionWidget()],
),
body: _data[index]['widget'] as Widget?,
);

if (Platform.isAndroid) {
widget = AndroidForegroundServiceWidget(
child: widget);
}

return widget;
}));
},
title: Text(
_data[index]['name'] as String,
Expand Down

0 comments on commit 32af54a

Please sign in to comment.