mirror of
https://github.com/ArcaneChat/android.git
synced 2026-07-03 14:05:24 +02:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3f442377e7 | |||
| 6ecb276a49 | |||
| 69c888f168 |
@@ -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"
|
||||
|
||||
@@ -166,10 +166,21 @@ public class WebViewActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
@@ -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;
|
||||
@@ -26,12 +29,18 @@ import android.webkit.WebSettings;
|
||||
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;
|
||||
@@ -53,6 +62,7 @@ import org.json.JSONObject;
|
||||
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;
|
||||
@@ -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, "");
|
||||
}
|
||||
@@ -268,6 +284,8 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
|
||||
|
||||
webView.loadUrl(this.baseURL + "/webxdc_bootstrap324567869.html?i=1&href=" + encodedHref);
|
||||
|
||||
initializeWebxdcMediaController();
|
||||
|
||||
Util.runOnAnyBackgroundThread(
|
||||
() -> {
|
||||
final DcChat chat = dcContext.getChat(dcAppMsg.getChatId());
|
||||
@@ -291,12 +309,72 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -737,5 +815,79 @@ public class WebxdcActivity extends WebViewActivity implements DcEventCenter.DcE
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
package org.thoughtcrime.securesms.service;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.OptIn;
|
||||
import androidx.media3.common.Player;
|
||||
import androidx.media3.common.SimpleBasePlayer;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.session.MediaSession;
|
||||
import androidx.media3.session.MediaSessionService;
|
||||
import androidx.media3.session.SessionCommand;
|
||||
import androidx.media3.session.SessionCommands;
|
||||
import androidx.media3.session.SessionResult;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import org.thoughtcrime.securesms.WebxdcActivity;
|
||||
|
||||
/**
|
||||
* A {@link MediaSessionService} for webxdc mini-apps playing audio in a WebView.
|
||||
*
|
||||
* <p>The actual audio is played by the WebView's internal audio engine. This service holds a
|
||||
* {@link MediaSession} backed by a stub {@link SimpleBasePlayer} purely to post the system media
|
||||
* notification and respond to hardware media keys / notification play-pause buttons.
|
||||
*
|
||||
* <p>Communication with {@link WebxdcActivity} uses custom {@link SessionCommand}s:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@code WEBXDC_AUDIO_STARTED} – audio began playing; args carry {@code title}, {@code
|
||||
* artist}, {@code msg_id}, {@code account_id} so the notification tap reopens the correct
|
||||
* webxdc instance.
|
||||
* <li>{@code WEBXDC_AUDIO_STOPPED} – audio fully stopped; service removes the notification.
|
||||
* <li>{@code WEBXDC_AUDIO_PAUSED} – audio paused by the app (not by the notification).
|
||||
* <li>{@code WEBXDC_AUDIO_RESUMED} – audio resumed by the app (not by the notification).
|
||||
* </ul>
|
||||
*
|
||||
* When the user presses play/pause in the notification, the stub player's
|
||||
* {@link SimpleBasePlayer#handleSetPlayWhenReady} relays the command back to {@link
|
||||
* WebxdcActivity} via a broadcast so the WebView can pause/resume its audio/video elements.
|
||||
*/
|
||||
@OptIn(markerClass = UnstableApi.class)
|
||||
public class WebxdcMediaSessionService extends MediaSessionService {
|
||||
|
||||
public static final String COMMAND_AUDIO_STARTED = "WEBXDC_AUDIO_STARTED";
|
||||
public static final String COMMAND_AUDIO_STOPPED = "WEBXDC_AUDIO_STOPPED";
|
||||
public static final String COMMAND_AUDIO_PAUSED = "WEBXDC_AUDIO_PAUSED";
|
||||
public static final String COMMAND_AUDIO_RESUMED = "WEBXDC_AUDIO_RESUMED";
|
||||
|
||||
/** Broadcast action sent when the system notification requests audio pause. */
|
||||
public static final String ACTION_NOTIFICATION_PAUSE =
|
||||
"org.thoughtcrime.securesms.WEBXDC_NOTIFICATION_PAUSE";
|
||||
|
||||
/** Broadcast action sent when the system notification requests audio resume. */
|
||||
public static final String ACTION_NOTIFICATION_RESUME =
|
||||
"org.thoughtcrime.securesms.WEBXDC_NOTIFICATION_RESUME";
|
||||
|
||||
private static final String TAG = WebxdcMediaSessionService.class.getSimpleName();
|
||||
|
||||
private StubPlayer stubPlayer;
|
||||
private MediaSession session;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Lifecycle
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
// Default session activity: open the conversation list. Updated by WEBXDC_AUDIO_STARTED.
|
||||
Intent defaultIntent =
|
||||
new Intent(this, org.thoughtcrime.securesms.ConversationListActivity.class);
|
||||
defaultIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
PendingIntent defaultPendingIntent =
|
||||
PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
defaultIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||
|
||||
stubPlayer = new StubPlayer();
|
||||
|
||||
session =
|
||||
new MediaSession.Builder(this, stubPlayer)
|
||||
.setSessionActivity(defaultPendingIntent)
|
||||
.setCallback(new SessionCallbackImpl())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public MediaSession onGetSession(MediaSession.ControllerInfo controllerInfo) {
|
||||
return session;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
if (session != null) {
|
||||
session.release();
|
||||
session = null;
|
||||
}
|
||||
if (stubPlayer != null) {
|
||||
stubPlayer.release();
|
||||
stubPlayer = null;
|
||||
}
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Stub player
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Minimal {@link SimpleBasePlayer} that reports playback state to the {@link MediaSession}
|
||||
* without controlling any audio engine. Play/pause commands from the notification are relayed
|
||||
* back to {@link WebxdcActivity} via a broadcast.
|
||||
*/
|
||||
@UnstableApi
|
||||
private final class StubPlayer extends SimpleBasePlayer {
|
||||
|
||||
private boolean playWhenReady = false;
|
||||
private int playbackState = Player.STATE_IDLE;
|
||||
private ImmutableList<MediaItemData> playlist = ImmutableList.of();
|
||||
|
||||
StubPlayer() {
|
||||
super(WebxdcMediaSessionService.this.getMainLooper());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected State getState() {
|
||||
State.Builder builder =
|
||||
new State.Builder()
|
||||
.setAvailableCommands(
|
||||
new Player.Commands.Builder()
|
||||
.addAll(Player.COMMAND_PLAY_PAUSE, Player.COMMAND_STOP)
|
||||
.build())
|
||||
.setPlayWhenReady(playWhenReady, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST)
|
||||
.setPlaybackState(playbackState)
|
||||
.setPlaylist(playlist);
|
||||
if (!playlist.isEmpty()) {
|
||||
builder.setCurrentMediaItemIndex(0);
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/** Called when the user presses play/pause in the system notification. */
|
||||
@NonNull
|
||||
@Override
|
||||
protected ListenableFuture<?> handleSetPlayWhenReady(boolean play) {
|
||||
playWhenReady = play;
|
||||
Intent broadcast =
|
||||
new Intent(play ? ACTION_NOTIFICATION_RESUME : ACTION_NOTIFICATION_PAUSE);
|
||||
broadcast.setPackage(getPackageName());
|
||||
sendBroadcast(broadcast);
|
||||
invalidateState();
|
||||
return Futures.immediateFuture(null);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected ListenableFuture<?> handleStop() {
|
||||
playWhenReady = false;
|
||||
playbackState = Player.STATE_IDLE;
|
||||
playlist = ImmutableList.of();
|
||||
Intent broadcast = new Intent(ACTION_NOTIFICATION_PAUSE);
|
||||
broadcast.setPackage(getPackageName());
|
||||
sendBroadcast(broadcast);
|
||||
invalidateState();
|
||||
return Futures.immediateFuture(null);
|
||||
}
|
||||
|
||||
void setPlaying(String title, String artist) {
|
||||
playWhenReady = true;
|
||||
playbackState = Player.STATE_READY;
|
||||
playlist = buildPlaylist(title, artist);
|
||||
invalidateState();
|
||||
}
|
||||
|
||||
void setPaused() {
|
||||
playWhenReady = false;
|
||||
playbackState = Player.STATE_READY;
|
||||
invalidateState();
|
||||
}
|
||||
|
||||
void setStopped() {
|
||||
playWhenReady = false;
|
||||
playbackState = Player.STATE_IDLE;
|
||||
playlist = ImmutableList.of();
|
||||
invalidateState();
|
||||
}
|
||||
|
||||
private ImmutableList<MediaItemData> buildPlaylist(String title, String artist) {
|
||||
androidx.media3.common.MediaMetadata metadata =
|
||||
new androidx.media3.common.MediaMetadata.Builder()
|
||||
.setTitle(title)
|
||||
.setArtist(artist)
|
||||
.build();
|
||||
androidx.media3.common.MediaItem mediaItem =
|
||||
new androidx.media3.common.MediaItem.Builder()
|
||||
.setMediaId("webxdc_audio")
|
||||
.setMediaMetadata(metadata)
|
||||
.build();
|
||||
return ImmutableList.of(
|
||||
new MediaItemData.Builder("webxdc_audio").setMediaItem(mediaItem).build());
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Session callback
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private final class SessionCallbackImpl implements MediaSession.Callback {
|
||||
|
||||
@OptIn(markerClass = UnstableApi.class)
|
||||
@NonNull
|
||||
@Override
|
||||
public MediaSession.ConnectionResult onConnect(
|
||||
@NonNull MediaSession session, @NonNull MediaSession.ControllerInfo controller) {
|
||||
SessionCommands sessionCommands =
|
||||
MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS
|
||||
.buildUpon()
|
||||
.add(new SessionCommand(COMMAND_AUDIO_STARTED, new Bundle()))
|
||||
.add(new SessionCommand(COMMAND_AUDIO_STOPPED, new Bundle()))
|
||||
.add(new SessionCommand(COMMAND_AUDIO_PAUSED, new Bundle()))
|
||||
.add(new SessionCommand(COMMAND_AUDIO_RESUMED, new Bundle()))
|
||||
.build();
|
||||
|
||||
return new MediaSession.ConnectionResult.AcceptedResultBuilder(session)
|
||||
.setAvailableSessionCommands(sessionCommands)
|
||||
.build();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ListenableFuture<SessionResult> onCustomCommand(
|
||||
@NonNull MediaSession session,
|
||||
@NonNull MediaSession.ControllerInfo controller,
|
||||
@NonNull SessionCommand customCommand,
|
||||
@NonNull Bundle args) {
|
||||
switch (customCommand.customAction) {
|
||||
case COMMAND_AUDIO_STARTED:
|
||||
handleAudioStarted(args);
|
||||
break;
|
||||
case COMMAND_AUDIO_STOPPED:
|
||||
if (stubPlayer != null) stubPlayer.setStopped();
|
||||
break;
|
||||
case COMMAND_AUDIO_PAUSED:
|
||||
if (stubPlayer != null) stubPlayer.setPaused();
|
||||
break;
|
||||
case COMMAND_AUDIO_RESUMED:
|
||||
if (stubPlayer != null)
|
||||
stubPlayer.setPlaying(args.getString("title", ""), args.getString("artist", ""));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return Futures.immediateFuture(new SessionResult(SessionResult.RESULT_SUCCESS));
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Command handlers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private void handleAudioStarted(Bundle args) {
|
||||
String title = args.getString("title", "");
|
||||
String artist = args.getString("artist", "");
|
||||
|
||||
if (args.containsKey("msg_id")) {
|
||||
int msgId = args.getInt("msg_id");
|
||||
int accountId = args.getInt("account_id", 0);
|
||||
updateSessionActivity(accountId, msgId);
|
||||
}
|
||||
if (stubPlayer != null) {
|
||||
stubPlayer.setPlaying(title, artist);
|
||||
}
|
||||
Log.i(TAG, "Audio started: title=" + title + " artist=" + artist);
|
||||
}
|
||||
|
||||
@OptIn(markerClass = UnstableApi.class)
|
||||
private void updateSessionActivity(int accountId, int msgId) {
|
||||
try {
|
||||
Intent intent = new Intent(this, WebxdcActivity.class);
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.putExtra("accountId", accountId);
|
||||
intent.putExtra("appMessageId", msgId);
|
||||
intent.putExtra("hideActionBar", false);
|
||||
intent.putExtra("href", "");
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
PendingIntent pendingIntent =
|
||||
PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||
if (session != null) {
|
||||
session.setSessionActivity(pendingIntent);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Failed to update session activity", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -181,3 +181,29 @@ window.webxdc = (() => {
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
||||
// Audio/video media session integration: notify Android when media plays/pauses/stops.
|
||||
(function() {
|
||||
function setupMediaListeners(doc) {
|
||||
var elements = doc.querySelectorAll('audio, video');
|
||||
for (var i = 0; i < elements.length; i++) {
|
||||
(function(el) {
|
||||
if (el._arcaneMediaListened) return;
|
||||
el._arcaneMediaListened = true;
|
||||
el.addEventListener('play', function() {
|
||||
if (window.InternalJSApi) InternalJSApi.notifyAudioStarted(document.title || '');
|
||||
});
|
||||
el.addEventListener('pause', function() {
|
||||
if (window.InternalJSApi) InternalJSApi.notifyAudioPaused();
|
||||
});
|
||||
el.addEventListener('ended', function() {
|
||||
if (window.InternalJSApi) InternalJSApi.notifyAudioStopped();
|
||||
});
|
||||
})(elements[i]);
|
||||
}
|
||||
}
|
||||
// Poll periodically so dynamically created audio/video elements are also detected.
|
||||
setInterval(function() {
|
||||
try { setupMediaListeners(document); } catch(e) {}
|
||||
}, 2000);
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user