Skip to content

Commit

Permalink
fix: run audio sources in a background service (#631)
Browse files Browse the repository at this point in the history
  • Loading branch information
kevmo314 authored Aug 22, 2022
1 parent 617c57e commit c5047da
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 95 deletions.
5 changes: 5 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,10 @@
<meta-data
android:name="flutterEmbedding"
android:value="2" />

<service
android:name=".AudioService"
android:enabled="true"
android:foregroundServiceType="mediaPlayback" />
</application>
</manifest>
161 changes: 161 additions & 0 deletions android/app/src/main/kotlin/com/rtirl/chat/AudioService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package com.rtirl.chat

import android.app.NotificationChannel
import android.app.NotificationManager
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.graphics.PixelFormat
import android.os.Build
import android.os.IBinder
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.webkit.*
import androidx.core.app.NotificationCompat
import io.flutter.Log

class AudioService : Service() {
companion object {
private const val NOTIFICATION_ID = 68448
private const val CHANNEL_ID = "AudioSources"
private const val PACKAGE_NAME = BuildConfig.APPLICATION_ID + ".AudioService"
const val ACTION_START_SERVICE = "$PACKAGE_NAME.start_service"
}

private val views = HashMap<String, WebView>()

private val notification: NotificationCompat.Builder
get() {
val intent = Intent(this, getMainActivityClass(this))

val builder = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("RealtimeChat Audio Sources")
.setContentText("We're keeping your audio sources alive fam.")
.setOngoing(true)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setColorized(true)
.setColor(0xFF009FDF.toInt())
.setSmallIcon(R.drawable.notification_icon)
.setWhen(System.currentTimeMillis())
.setContentIntent(
PendingIntent.getActivity(
this, 0, intent,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
)
)

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder.setChannelId(CHANNEL_ID)
}

return builder
}

private fun getMainActivityClass(context: Context): Class<*>? {
val packageName = context.packageName
val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)
val className = launchIntent?.component?.className ?: return null

return try {
Class.forName(className)
} catch (e: ClassNotFoundException) {
e.printStackTrace()
null
}
}

override fun onBind(p0: Intent?): IBinder? {
return null
}

@SuppressLint("SetJavaScriptEnabled")
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val urls = intent?.getStringArrayListExtra("urls")?.toHashSet()

if (urls != null) {
val wm = getSystemService(WINDOW_SERVICE) as WindowManager

val add = (urls subtract views.keys)
val remove = (views.keys subtract urls)
add.forEach {
val view = WebView(this)
view.settings.javaScriptEnabled = true
view.settings.mediaPlaybackRequiresUserGesture = false
view.settings.domStorageEnabled = true
view.settings.databaseEnabled = true
view.settings.mixedContentMode = WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE
view.webChromeClient = object : WebChromeClient() {
override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean {
Log.d("WebView", consoleMessage.message())
return true
}
}
view.setLayerType(View.LAYER_TYPE_HARDWARE, null)
view.loadUrl(it)
view.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?
): Boolean {
return false
}
}

val params = WindowManager.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY else WindowManager.LayoutParams.TYPE_PHONE,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
PixelFormat.TRANSPARENT
)

params.x = 0
params.y = 0
params.width = 0
params.height = 0

wm.addView(
view, params
)
views[it] = view
}
remove.forEach {
wm.removeView(views[it])
views.remove(it)?.destroy()
}
} else {
views.forEach { (_, view) -> view.reload() }
}

val nm = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

return if (views.isNotEmpty()) {
// ensure the notification is shown
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val mChannel = NotificationChannel(
CHANNEL_ID,
"Audio Sources",
NotificationManager.IMPORTANCE_MIN
)
mChannel.setSound(null, null)
nm.createNotificationChannel(mChannel)
}
startForeground(NOTIFICATION_ID, notification.build())
START_STICKY
} else {
// ensure the notification is removed
stopForeground(true)
nm.cancel(NOTIFICATION_ID)
stopSelf()
START_NOT_STICKY
}
}

}
80 changes: 12 additions & 68 deletions android/app/src/main/kotlin/com/rtirl/chat/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,95 +1,39 @@
package com.rtirl.chat

import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.graphics.PixelFormat
import android.net.Uri
import android.os.Build
import android.provider.Settings
import android.view.View
import android.view.WindowManager
import android.webkit.*
import androidx.annotation.NonNull
import io.flutter.Log
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel


class MainActivity : FlutterActivity() {
private val views = HashMap<String, WebView>()

override fun onDestroy() {
// there is no corresponding onCreate because Flutter will send us the urls.
super.onDestroy()

val wm = getSystemService(WINDOW_SERVICE) as WindowManager
views.values.forEach { wm.removeView(it) }
}

@SuppressLint("SetJavaScriptEnabled")
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
"com.rtirl.chat/audio"
).setMethodCallHandler { call, result ->
val wm = getSystemService(WINDOW_SERVICE) as WindowManager
when (call.method) {
"set" -> {
val urls = (call.argument<List<String>>("urls") ?: listOf()).toHashSet()
val add = (urls subtract views.keys)
val remove = (views.keys subtract urls)
add.forEach {
val view = WebView(context)
view.settings.javaScriptEnabled = true
view.settings.mediaPlaybackRequiresUserGesture = false
view.settings.domStorageEnabled = true
view.settings.databaseEnabled = true
view.settings.mixedContentMode = WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE
view.webChromeClient = object : WebChromeClient() {
override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean {
Log.d("WebView", consoleMessage.message())
return true
}
}
view.setLayerType(View.LAYER_TYPE_HARDWARE, null)
view.loadUrl(it)
view.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?
): Boolean {
return false
}
}
val intent = Intent(this, AudioService::class.java)
intent.putStringArrayListExtra(
"urls",
ArrayList(call.argument<List<String>>("urls") ?: listOf())
)
intent.action = AudioService.ACTION_START_SERVICE
startService(intent)

wm.addView(
view, WindowManager.LayoutParams(
0,
0,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY else WindowManager.LayoutParams.TYPE_PHONE,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
PixelFormat.OPAQUE
)
)
views[it] = view
result.success(true)
}
remove.forEach {
wm.removeView(views[it])
views.remove(it)?.destroy()
}
result.success(true)
}
"reload" -> {
val url = call.argument<String>("url")
if (url == null || views[url] == null) {
result.success(false)
} else {
views[url]?.reload()
result.success(true)
}
val intent = Intent(this, AudioService::class.java)
intent.action = AudioService.ACTION_START_SERVICE
startService(intent)

result.success(true)
}
"hasPermission" -> {
result.success(
Expand Down
19 changes: 0 additions & 19 deletions lib/models/audio.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import 'dart:io';

import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/material.dart';
import 'package:flutter_background/flutter_background.dart';
import 'package:rtchat/audio_channel.dart';
import 'package:rtchat/models/adapters/profiles.dart';
import 'package:rtchat/models/channels.dart';
Expand Down Expand Up @@ -68,7 +67,6 @@ class AudioModel extends ChangeNotifier {
_hostChannelStateSubscription = null;
_isOnline = false;
_syncWebViews();
_toggleBackground(false);
notifyListeners();
return;
}
Expand All @@ -77,27 +75,10 @@ class AudioModel extends ChangeNotifier {
.listen((isOnline) {
_isOnline = isOnline;
_syncWebViews();
_toggleBackground(isOnline);
notifyListeners();
});
}

// this isn't really the best place to put this function since it applies
// outside audio, but it's a convenient place since we already have the listener.
void _toggleBackground(bool enable) async {
const androidConfig = FlutterBackgroundAndroidConfig(
notificationTitle: "RealtimeChat",
notificationText: "RealtimeChat is running in the background",
);
if (enable) {
if (await FlutterBackground.initialize(androidConfig: androidConfig)) {
await FlutterBackground.enableBackgroundExecution();
}
} else if (FlutterBackground.isBackgroundExecutionEnabled) {
await FlutterBackground.disableBackgroundExecution();
}
}

bool get isSettingsVisible => _isSettingsVisible;

set isSettingsVisible(bool value) {
Expand Down
7 changes: 0 additions & 7 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -251,13 +251,6 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_background:
dependency: "direct main"
description:
name: flutter_background
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
flutter_custom_tabs:
dependency: "direct main"
description:
Expand Down
1 change: 0 additions & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ dependencies:
path_provider: ^2.0.10
in_app_purchase: ^3.0.4
firebase_database: ^9.0.18
flutter_background: ^1.1.0
flutter_markdown: ^0.6.10+2
flutter_localized_locales: ^2.0.3
flutter_custom_tabs: ^1.0.4
Expand Down

0 comments on commit c5047da

Please sign in to comment.