Skip to content
Merged
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
11 changes: 11 additions & 0 deletions src-tauri/gen/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />

<!-- Foreground service for ongoing calls (microphone|mediaPlayback on API 34+) -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />

<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.microphone" android:required="false" />

Expand Down Expand Up @@ -111,6 +117,11 @@
<!-- DEEP LINK PLUGIN. AUTO-GENERATED. DO NOT REMOVE. -->
</activity>

<service
android:name="moe.sable.client.CallForegroundService"
android:exported="false"
android:foregroundServiceType="microphone|mediaPlayback" />

<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package moe.sable.client

import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Intent
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat

class CallForegroundService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
createNotificationChannel()
startForeground(NOTIFICATION_ID, buildNotification())
return START_NOT_STICKY
}

override fun onBind(intent: Intent?): IBinder? = null

override fun onDestroy() {
stopForeground(true)
super.onDestroy()
}

private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return

val channel = NotificationChannel(
CHANNEL_ID,
"Calls",
NotificationManager.IMPORTANCE_LOW
)
getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
}

private fun buildNotification(): Notification {
return NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.mipmap.ic_notification)
.setContentTitle("Call in progress")
.setContentText("Sable is keeping your call active")
.setCategory(Notification.CATEGORY_CALL)
.setOngoing(true)
.setOnlyAlertOnce(true)
.build()
}

companion object {
private const val CHANNEL_ID = "call_foreground"
private const val NOTIFICATION_ID = 1395
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import android.provider.OpenableColumns
import android.webkit.WebView
import androidx.activity.OnBackPressedCallback
import androidx.activity.enableEdgeToEdge
import androidx.core.content.ContextCompat
import androidx.core.content.IntentCompat
import androidx.core.view.WindowCompat
import java.io.File
Expand Down Expand Up @@ -208,5 +209,24 @@ class MainActivity : TauriActivity() {
}
}
}

@JvmStatic
fun startCallForegroundServiceNative() {
val activity = instance ?: return
activity.runOnUiThread {
ContextCompat.startForegroundService(
activity,
Intent(activity, CallForegroundService::class.java)
)
}
}

@JvmStatic
fun stopCallForegroundServiceNative() {
val activity = instance ?: return
activity.runOnUiThread {
activity.stopService(Intent(activity, CallForegroundService::class.java))
}
}
}
}
1 change: 1 addition & 0 deletions src-tauri/ios-project.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ targets:
- UIInterfaceOrientationLandscapeRight
CFBundleShortVersionString: {{apple.bundle-version-short}}
CFBundleVersion: "{{apple.bundle-version}}"
UIBackgroundModes: [audio]
# Mirrors plugins.deep-link in tauri.conf.json: the plugin only patches
# these in when cargo reruns its build script, so a fresh `tauri ios
# init` silently loses them unless they live here.
Expand Down
4 changes: 4 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,10 @@ pub fn run() {
mobile::set_status_bar_color,
#[cfg(target_os = "android")]
mobile::set_navigation_bar_color,
#[cfg(target_os = "android")]
mobile::start_call_foreground_service,
#[cfg(target_os = "android")]
mobile::stop_call_foreground_service,
#[cfg(target_os = "ios")]
ios::haptic_feedback,
#[cfg(any(target_os = "android", target_os = "ios"))]
Expand Down
23 changes: 23 additions & 0 deletions src-tauri/src/mobile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ pub fn set_navigation_bar_color(color: u32) -> Result<(), String> {
call_bar_color("setNavigationBarColorNative", color)
}

#[tauri::command]
pub fn start_call_foreground_service() -> Result<(), String> {
call_static_method("startCallForegroundServiceNative")
}

#[tauri::command]
pub fn stop_call_foreground_service() -> Result<(), String> {
call_static_method("stopCallForegroundServiceNative")
}

/// `kind` is "notification" or "invite"; mapped to an int to avoid JNI string
/// marshalling (mirrors set_*_bar_color).
pub(crate) fn play_notification_sound(kind: String) -> Result<(), String> {
Expand Down Expand Up @@ -75,3 +85,16 @@ fn call_bar_color(method: &str, color: u32) -> Result<(), String> {

Ok(())
}

fn call_static_method(method: &str) -> Result<(), String> {
let vm = JAVA_VM.get().ok_or("java vm not initialized")?;
let mut env = vm.attach_current_thread().map_err(|e| e.to_string())?;

let result = env.call_static_method("moe/sable/client/MainActivity", method, "()V", &[]);
if result.is_err() {
let _ = env.exception_clear();
}
result.map_err(|e| e.to_string())?;

Ok(())
}
12 changes: 11 additions & 1 deletion src/app/plugins/call/CallEmbed.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { MatrixClient, MatrixEvent, Room } from '$types/matrix-sdk';
import { ClientEvent, KnownMembership, MatrixEventEvent, RoomStateEvent } from '$types/matrix-sdk';
import { invoke } from '@tauri-apps/api/core';
import type { IRoomEvent, IWidget, WidgetDriver } from 'matrix-widget-api';
import {
ClientWidgetApi,
Expand All @@ -10,7 +11,7 @@ import {
} from 'matrix-widget-api';
import { CallWidgetDriver } from './CallWidgetDriver';
import { trimTrailingSlash } from '../../utils/common';
import { getWindowOrigin } from '../../utils/platform';
import { getWindowOrigin, isAndroidTauri } from '../../utils/platform';
import type { ElementCallThemeKind, ElementMediaStateDetail } from './types';
import { color, config } from 'folds';
import { ElementCallIntent, ElementWidgetActions } from './types';
Expand Down Expand Up @@ -355,6 +356,7 @@ export class CallEmbed {
*/
public dispose(): void {
debugLog.info('call', 'Disposing call widget', { roomId: this.roomId });
this.invokeForegroundService('stop_call_foreground_service');
this.disposables.forEach((disposable) => {
disposable();
});
Expand All @@ -373,10 +375,18 @@ export class CallEmbed {
this.call.transport.reply(evt.detail as IWidgetApiRequest, {});
debugLog.info('call', 'Call joined', { roomId: this.roomId });
this.joined = true;
this.invokeForegroundService('start_call_foreground_service');
this.applyStyles();
this.control.startObserving();
}

private invokeForegroundService(
command: 'start_call_foreground_service' | 'stop_call_foreground_service'
): void {
if (!isAndroidTauri()) return;
void invoke(command).catch(() => {});
}

private applyStyles(): void {
const doc = this.document;
if (!doc) return;
Expand Down
Loading