Skip to content
Draft
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
9 changes: 9 additions & 0 deletions src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,15 @@
</intent-filter>
</service>

<service
android:name=".service.WebxdcMediaSessionService"
android:foregroundServiceType="mediaPlayback"
android:exported="true">
<intent-filter>
<action android:name="androidx.media3.session.MediaSessionService" />
</intent-filter>
</service>

<service
android:name=".calls.CallService"
android:enabled="true"
Expand Down
13 changes: 12 additions & 1 deletion src/main/java/org/thoughtcrime/securesms/WebViewActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -166,10 +166,21 @@ protected void setForceDark() {
}
}

/**
* Returns true if the WebView should be paused when the activity is paused.
* Subclasses can override to keep the WebView running in the background (e.g., during audio
* playback).
*/
protected boolean pauseWebViewOnPause() {
return true;
}

@Override
protected void onPause() {
super.onPause();
webView.onPause();
if (pauseWebViewOnPause()) {
webView.onPause();
}
}

@Override
Expand Down
152 changes: 152 additions & 0 deletions src/main/java/org/thoughtcrime/securesms/WebxdcActivity.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package org.thoughtcrime.securesms;

import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.graphics.Bitmap;
Expand All @@ -26,12 +29,18 @@
import android.webkit.WebView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.ActionBar;
import androidx.core.app.TaskStackBuilder;
import androidx.core.content.ContextCompat;
import androidx.core.content.pm.ShortcutInfoCompat;
import androidx.core.content.pm.ShortcutManagerCompat;
import androidx.core.graphics.drawable.IconCompat;
import androidx.media3.session.MediaController;
import androidx.media3.session.SessionCommand;
import androidx.media3.session.SessionToken;
import com.google.common.util.concurrent.ListenableFuture;
import chat.delta.rpc.Rpc;
import chat.delta.rpc.RpcException;
import com.b44t.messenger.DcChat;
Expand All @@ -53,6 +62,7 @@
import org.thoughtcrime.securesms.connect.AccountManager;
import org.thoughtcrime.securesms.connect.DcEventCenter;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.service.WebxdcMediaSessionService;
import org.thoughtcrime.securesms.util.IntentUtils;
import org.thoughtcrime.securesms.util.JsonUtils;
import org.thoughtcrime.securesms.util.MediaUtil;
Expand Down Expand Up @@ -81,6 +91,12 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE

private TextToSpeech tts;

private boolean isAudioPlaying = false;
private String currentAudioTitle = "";
private @Nullable MediaController webxdcMediaController;
private @Nullable ListenableFuture<MediaController> webxdcMediaControllerFuture;
private @Nullable BroadcastReceiver notificationControlReceiver;

public static void openMaps(Context context, int chatId) {
openMaps(context, chatId, "");
}
Expand Down Expand Up @@ -268,6 +284,8 @@ public boolean onShowFileChooser(

webView.loadUrl(this.baseURL + "/webxdc_bootstrap324567869.html?i=1&href=" + encodedHref);

initializeWebxdcMediaController();

Util.runOnAnyBackgroundThread(
() -> {
final DcChat chat = dcContext.getChat(dcAppMsg.getChatId());
Expand All @@ -291,12 +309,72 @@ protected void onPause() {
DcHelper.getNotificationCenter(this).clearVisibleWebxdc();
}

@Override
protected boolean pauseWebViewOnPause() {
// Keep the WebView JS timers/audio running in the background when audio is playing,
// mirroring what browsers do when a tab has media playing.
return !isAudioPlaying;
}

private void initializeWebxdcMediaController() {
SessionToken sessionToken =
new SessionToken(this, new ComponentName(this, WebxdcMediaSessionService.class));
webxdcMediaControllerFuture =
new MediaController.Builder(this, sessionToken).buildAsync();
webxdcMediaControllerFuture.addListener(
() -> {
try {
webxdcMediaController = webxdcMediaControllerFuture.get();
} catch (Exception e) {
Log.e(TAG, "Error connecting to WebxdcMediaSessionService", e);
}
},
ContextCompat.getMainExecutor(this));

// Register receiver for play/pause commands from the system notification.
notificationControlReceiver =
new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (WebxdcMediaSessionService.ACTION_NOTIFICATION_PAUSE.equals(intent.getAction())) {
webView.evaluateJavascript(
"document.querySelectorAll('audio,video').forEach(function(el){el.pause();});",
null);
} else if (WebxdcMediaSessionService.ACTION_NOTIFICATION_RESUME.equals(
intent.getAction())) {
webView.evaluateJavascript(
"document.querySelectorAll('audio,video').forEach(function(el){el.play();});",
null);
}
}
};
IntentFilter filter = new IntentFilter();
filter.addAction(WebxdcMediaSessionService.ACTION_NOTIFICATION_PAUSE);
filter.addAction(WebxdcMediaSessionService.ACTION_NOTIFICATION_RESUME);
ContextCompat.registerReceiver(
this, notificationControlReceiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED);
}

@Override
protected void onDestroy() {
lastOpenTime = System.currentTimeMillis();
DcHelper.getEventCenter(this.getApplicationContext()).removeObservers(this);
leaveRealtimeChannel();
tts.shutdown();
if (isAudioPlaying && webxdcMediaController != null) {
webxdcMediaController.sendCustomCommand(
new SessionCommand(WebxdcMediaSessionService.COMMAND_AUDIO_STOPPED, new Bundle()),
Bundle.EMPTY);
}
if (notificationControlReceiver != null) {
unregisterReceiver(notificationControlReceiver);
notificationControlReceiver = null;
}
if (webxdcMediaControllerFuture != null) {
MediaController.releaseFuture(webxdcMediaControllerFuture);
webxdcMediaControllerFuture = null;
webxdcMediaController = null;
}
super.onDestroy();
}

Expand Down Expand Up @@ -737,5 +815,79 @@ public void ttsSpeak(String text, String lang) {
if (lang != null && !lang.isEmpty()) tts.setLanguage(Locale.forLanguageTag(lang));
tts.speak(text, TextToSpeech.QUEUE_FLUSH, null, null);
}

/**
* @noinspection unused
*/
@JavascriptInterface
public void notifyAudioStarted(String title) {
Util.runOnMain(
() -> {
if (webxdcMediaController == null) return;
isAudioPlaying = true;
currentAudioTitle = title;
Bundle args = new Bundle();
args.putString("title", title);
args.putString(
"artist",
WebxdcActivity.this.dcAppMsg.getWebxdcInfo().optString("name", ""));
args.putInt("msg_id", WebxdcActivity.this.dcAppMsg.getId());
args.putInt("account_id", WebxdcActivity.this.dcContext.getAccountId());
webxdcMediaController.sendCustomCommand(
new SessionCommand(WebxdcMediaSessionService.COMMAND_AUDIO_STARTED, new Bundle()),
args);
});
}

/**
* @noinspection unused
*/
@JavascriptInterface
public void notifyAudioStopped() {
Util.runOnMain(
() -> {
if (webxdcMediaController == null) return;
isAudioPlaying = false;
currentAudioTitle = "";
webxdcMediaController.sendCustomCommand(
new SessionCommand(WebxdcMediaSessionService.COMMAND_AUDIO_STOPPED, new Bundle()),
Bundle.EMPTY);
});
}

/**
* @noinspection unused
*/
@JavascriptInterface
public void notifyAudioPaused() {
Util.runOnMain(
() -> {
if (webxdcMediaController == null) return;
isAudioPlaying = false;
webxdcMediaController.sendCustomCommand(
new SessionCommand(WebxdcMediaSessionService.COMMAND_AUDIO_PAUSED, new Bundle()),
Bundle.EMPTY);
});
}

/**
* @noinspection unused
*/
@JavascriptInterface
public void notifyAudioResumed() {
Util.runOnMain(
() -> {
if (webxdcMediaController == null) return;
isAudioPlaying = true;
Bundle args = new Bundle();
args.putString("title", currentAudioTitle);
args.putString(
"artist",
WebxdcActivity.this.dcAppMsg.getWebxdcInfo().optString("name", ""));
webxdcMediaController.sendCustomCommand(
new SessionCommand(WebxdcMediaSessionService.COMMAND_AUDIO_RESUMED, new Bundle()),
args);
});
}
}
}
Loading