-
Notifications
You must be signed in to change notification settings - Fork 60
Meet: Android foreground service to survive screen lock #281
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| package io.livekit.unity.sample; | ||
|
|
||
| import android.app.Notification; | ||
| import android.app.NotificationChannel; | ||
| import android.app.NotificationManager; | ||
| import android.app.Service; | ||
| import android.content.Context; | ||
| import android.content.Intent; | ||
| import android.os.Build; | ||
| import android.os.IBinder; | ||
|
|
||
| public class LiveKitForegroundService extends Service { | ||
|
|
||
| private static final String CHANNEL_ID = "livekit_call"; | ||
| private static final int NOTIFICATION_ID = 1701; | ||
|
|
||
| public static void start(Context context) { | ||
| Intent intent = new Intent(context, LiveKitForegroundService.class); | ||
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | ||
| context.startForegroundService(intent); | ||
| } else { | ||
| context.startService(intent); | ||
| } | ||
| } | ||
|
|
||
| public static void stop(Context context) { | ||
| Intent intent = new Intent(context, LiveKitForegroundService.class); | ||
| context.stopService(intent); | ||
| } | ||
|
|
||
| @Override | ||
| public void onCreate() { | ||
| super.onCreate(); | ||
| ensureChannel(); | ||
| startForeground(NOTIFICATION_ID, buildNotification()); | ||
| } | ||
|
|
||
| @Override | ||
| public int onStartCommand(Intent intent, int flags, int startId) { | ||
| return START_STICKY; | ||
| } | ||
|
|
||
| @Override | ||
| public IBinder onBind(Intent intent) { | ||
| return null; | ||
| } | ||
|
|
||
| private void ensureChannel() { | ||
| if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return; | ||
| NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); | ||
| if (nm == null) return; | ||
| if (nm.getNotificationChannel(CHANNEL_ID) != null) return; | ||
| NotificationChannel channel = new NotificationChannel( | ||
| CHANNEL_ID, | ||
| "Active call", | ||
| NotificationManager.IMPORTANCE_LOW); | ||
| channel.setShowBadge(false); | ||
| nm.createNotificationChannel(channel); | ||
| } | ||
|
|
||
| private Notification buildNotification() { | ||
| Notification.Builder builder; | ||
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | ||
| builder = new Notification.Builder(this, CHANNEL_ID); | ||
| } else { | ||
| builder = new Notification.Builder(this); | ||
| } | ||
| return builder | ||
| .setContentTitle("LiveKit call") | ||
| .setContentText("Connected — running in the background") | ||
| .setSmallIcon(android.R.drawable.ic_menu_call) | ||
| .setOngoing(true) | ||
| .build(); | ||
| } | ||
| } | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| #if UNITY_ANDROID && !UNITY_EDITOR | ||
| using UnityEngine; | ||
|
|
||
| /// <summary> | ||
| /// Starts/stops an Android foreground service so the OS keeps the process out of Doze | ||
| /// while the user is in a LiveKit call. Without it, the screen lock pauses the network | ||
| /// long enough for the SDK to exhaust its 10 reconnect attempts and disconnect. | ||
| /// Call sites must guard with `#if UNITY_ANDROID && !UNITY_EDITOR`. | ||
| /// </summary> | ||
| public static class AndroidBackgroundService | ||
| { | ||
| private const string ServiceClassName = "io.livekit.unity.sample.LiveKitForegroundService"; | ||
|
|
||
| public static void Start() | ||
| { | ||
| WithActivity((cls, activity) => cls.CallStatic("start", activity)); | ||
| } | ||
|
|
||
| public static void Stop() | ||
| { | ||
| WithActivity((cls, activity) => cls.CallStatic("stop", activity)); | ||
| } | ||
|
|
||
| private static void WithActivity(System.Action<AndroidJavaClass, AndroidJavaObject> body) | ||
| { | ||
| try | ||
| { | ||
| using var player = new AndroidJavaClass("com.unity3d.player.UnityPlayer"); | ||
| using var activity = player.GetStatic<AndroidJavaObject>("currentActivity"); | ||
| using var serviceClass = new AndroidJavaClass(ServiceClassName); | ||
| body(serviceClass, activity); | ||
| } | ||
| catch (System.Exception e) | ||
| { | ||
| Debug.LogWarning($"AndroidBackgroundService: {e.Message}"); | ||
| } | ||
| } | ||
| } | ||
| #endif |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -90,6 +90,9 @@ private void OnDestroy() | |
| } | ||
| CleanUpAllTracks(); | ||
| _webCamTexture?.Stop(); | ||
| #if UNITY_ANDROID && !UNITY_EDITOR | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not everybody uses MeetManager. Does this mean every client has to use this on their own?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is a decision still to be made. Depending on the downsides of this solution, it might be a client decision if this behaviour is wanted. If there are no downsides though, I would rather want to make it the generic behaviour. |
||
| AndroidBackgroundService.Stop(); | ||
| #endif | ||
| } | ||
|
|
||
| #endregion | ||
|
|
@@ -110,6 +113,9 @@ private void OnEndCall() | |
| _room = null; | ||
| _localId = null; | ||
| buttonBar.SetConnected(false); | ||
| #if UNITY_ANDROID && !UNITY_EDITOR | ||
| AndroidBackgroundService.Stop(); | ||
| #endif | ||
| } | ||
|
|
||
| private void OnToggleCamera() | ||
|
|
@@ -183,6 +189,9 @@ private IEnumerator ConnectToRoom() | |
| Debug.Log($"Connected to {_room.Name}"); | ||
| _localId = _room.LocalParticipant.Identity; | ||
| buttonBar.SetConnected(true); | ||
| #if UNITY_ANDROID && !UNITY_EDITOR | ||
| AndroidBackgroundService.Start(); | ||
| #endif | ||
|
|
||
| EnsureParticipantTile(_localId); | ||
| foreach (var remote in _room.RemoteParticipants.Values) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess this is hardcoded for now just for testing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes this was an initial try how this fix behaves in practice. I am investigating other options as well, since I am worried this one could drain the battery if you lock your screen.
In this draft PR it is also pretty hard coded. If this is the best solution, I would want this to go into the SDK and not much work for the client code.