Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
a9832c9
Make audio view use Media3 for playback
wchen342 Jan 21, 2026
9a121b3
Fix UI update problems;
wchen342 Jan 23, 2026
4bd7432
Try to decouple view and business logics
wchen342 Jan 26, 2026
b74e793
Finish ViewModel for audio playback; Refine UI set up for AudioView
wchen342 Jan 27, 2026
7683408
Allow going back to previous activity from the notification; Support …
wchen342 Jan 28, 2026
11f3964
Add support for attachment draft; Distinguish between different messages
wchen342 Jan 28, 2026
59139ed
Add support for attachment draft; Distinguish between different messa…
wchen342 Jan 28, 2026
34be7aa
Cleanup imports
wchen342 Jan 28, 2026
1ff4e06
Merge branch 'main' into wch423/audio-background-play
adbenitez Jan 29, 2026
13374df
Bug fixes and minor changes
wchen342 Feb 3, 2026
10acb07
Make device messages and subscribed channels edge to edge
wchen342 Jan 29, 2026
93f12e7
Make message list respect bottom bar height
wchen342 Jan 29, 2026
f319ba2
Allow list scrolling to extend to edge
wchen342 Feb 3, 2026
aebd5c6
Merge branch 'main' into wch423/audio-background-play
adbenitez Feb 3, 2026
b66bc1f
Handle specific case when leaving group; also move floating button to…
wchen342 Feb 4, 2026
71ed333
Stop playback when attachment preview is removed;
wchen342 Feb 4, 2026
5e6fccf
Merge branch 'main' into wch423/edge-to-edge-device-channel
adbenitez Feb 5, 2026
2889266
Force dispatch all inset changes immediately in ConversationFragment
wchen342 Feb 5, 2026
cebfa12
Merge branch 'main' into wch423/edge-to-edge-device-channel
adbenitez Feb 5, 2026
82118db
Make inset changes only happen on initialization of activity
wchen342 Feb 5, 2026
57eead3
Merge branch 'main' into wch423/edge-to-edge-device-channel
adbenitez Feb 5, 2026
3386f5c
Make `!CanSend` -> `CanSend` still trigger Inset changes
wchen342 Feb 5, 2026
f022316
Merge branch 'main' into wch423/audio-background-play
adbenitez Feb 5, 2026
1c174b5
Stop draft audio playback when (1) the conversation exits; or (2) the…
wchen342 Feb 5, 2026
2704749
Merge pull request #4198 from deltachat/wch423/edge-to-edge-device-ch…
wchen342 Feb 5, 2026
9ffc904
more detailed call strings
r10s Feb 5, 2026
30124de
Merge branch 'main' into wch423/audio-background-play
adbenitez Feb 6, 2026
93427ba
Merge remote-tracking branch 'upstream/wch423/audio-background-play' …
adbenitez Feb 6, 2026
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
1 change: 1 addition & 0 deletions CHANGELOG-upstream.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
* Add indication for blocked contacts in user profile
* Allow to start calls with video disabled
* Show hint for empty contact search results
* Add background playing for voice messages and other audio files
* Fix: Show dialog if pasted QR codes are invalid
* Fix: Refresh chat list when returning from conversation if selected profile changed
* Fix: Update menu when using "select all" in contact selection
Expand Down
10 changes: 9 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
coreLibraryDesugaringEnabled true
}
packagingOptions {
jniLibs {
Expand Down Expand Up @@ -211,6 +212,10 @@ dependencies {
implementation "io.noties.markwon:inline-parser:$markwon_version"
implementation 'com.airbnb.android:lottie:4.2.2' // Lottie animations support.

coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'

def media3_version = "1.8.0" // 1.9.0 need minSdkVersion 23

implementation 'androidx.concurrent:concurrent-futures:1.3.0'
implementation 'androidx.sharetarget:sharetarget:1.2.0'
implementation 'androidx.webkit:webkit:1.14.0'
Expand All @@ -231,8 +236,11 @@ dependencies {
implementation 'androidx.work:work-runtime:2.9.1'
implementation 'androidx.emoji2:emoji2-emojipicker:1.5.0'
implementation 'com.google.guava:guava:31.1-android'
implementation 'com.google.android.exoplayer:exoplayer-core:2.19.1' // plays video and audio
implementation 'com.google.android.exoplayer:exoplayer-core:2.19.1' // FIXME: exoplayer dependencies kept for Video, but we shall migrate them at some point
implementation 'com.google.android.exoplayer:exoplayer-ui:2.19.1'
implementation "androidx.media3:media3-exoplayer:$media3_version"
implementation "androidx.media3:media3-session:$media3_version"
implementation "androidx.media3:media3-ui:$media3_version"
implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
implementation 'com.google.zxing:core:3.3.0' // fixed version to support SDK<24
implementation ('com.journeyapps:zxing-android-embedded:4.3.0') { transitive = false } // QR Code scanner
Expand Down
10 changes: 10 additions & 0 deletions src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />

<!-- force compiling emojipicker on sdk<21; runtime checks are required then -->
<uses-sdk tools:overrideLibrary="androidx.emoji2.emojipicker"/>
Expand Down Expand Up @@ -391,6 +392,15 @@
android:name=".service.FetchForegroundService"
android:foregroundServiceType="dataSync" />

<service
android:name=".service.AudioPlaybackService"
android:foregroundServiceType="mediaPlayback"
android:exported="true">
<intent-filter>
<action android:name="androidx.media3.session.MediaSessionService"/>
</intent-filter>
</service>
Comment on lines +395 to +402
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AudioPlaybackService is declared android:exported="true", which allows any other app to connect as a Media3 controller. Given the current onConnect implementation accepts all controllers and exposes a custom command, this becomes a security boundary issue. If external control isn't required, set exported="false"; otherwise explicitly restrict allowed controllers (e.g., same package UID / system UI) and only expose custom commands to trusted callers.

Copilot uses AI. Check for mistakes.

<receiver android:name=".notifications.MarkReadReceiver"
android:enabled="true"
android:exported="false">
Expand Down
59 changes: 58 additions & 1 deletion src/main/java/org/thoughtcrime/securesms/AllMediaActivity.java
Original file line number Diff line number Diff line change
@@ -1,35 +1,46 @@
package org.thoughtcrime.securesms;

import android.content.ComponentName;
import android.os.Bundle;
import android.util.Log;
import android.view.MenuItem;
import android.view.ViewGroup;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.view.ActionMode;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter;
import androidx.lifecycle.ViewModelProvider;
import androidx.media3.session.MediaController;
import androidx.media3.session.SessionCommand;
import androidx.media3.session.SessionToken;
import androidx.viewpager.widget.ViewPager;

import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcEvent;
import com.b44t.messenger.DcMsg;
import com.google.android.material.tabs.TabLayout;
import com.google.common.util.concurrent.ListenableFuture;

import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
import org.thoughtcrime.securesms.connect.DcEventCenter;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.service.AudioPlaybackService;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;

import java.util.ArrayList;

public class AllMediaActivity extends PassphraseRequiredActionBarActivity
implements DcEventCenter.DcEventDelegate
{
private static final String TAG = AllMediaActivity.class.getSimpleName();

public static final String CHAT_ID_EXTRA = "chat_id";
public static final String CONTACT_ID_EXTRA = "contact_id";
Expand Down Expand Up @@ -57,6 +68,10 @@ static class TabData {
private TabLayout tabLayout;
private ViewPager viewPager;

private @Nullable MediaController mediaController;
private ListenableFuture<MediaController> mediaControllerFuture;
private AudioPlaybackViewModel playbackViewModel;

@Override
protected void onPreCreate() {
dynamicTheme = new DynamicNoActionBarTheme();
Expand Down Expand Up @@ -91,11 +106,19 @@ protected void onCreate(Bundle bundle, boolean ready) {
DcEventCenter eventCenter = DcHelper.getEventCenter(this);
eventCenter.addObserver(DcContext.DC_EVENT_CHAT_MODIFIED, this);
eventCenter.addObserver(DcContext.DC_EVENT_CONTACTS_CHANGED, this);

playbackViewModel = new ViewModelProvider(this).get(AudioPlaybackViewModel.class);
initializeMediaController();
}

@Override
public void onDestroy() {
DcHelper.getEventCenter(this).removeObservers(this);
if (mediaController != null) {
MediaController.releaseFuture(mediaControllerFuture);
mediaController = null;
playbackViewModel.setMediaController(null);
}
super.onDestroy();
}

Expand Down Expand Up @@ -124,6 +147,40 @@ private void initializeResources() {
this.tabLayout = ViewUtil.findById(this, R.id.tab_layout);
}

private void initializeMediaController() {
SessionToken sessionToken = new SessionToken(this,
new ComponentName(this, AudioPlaybackService.class));
mediaControllerFuture = new MediaController.Builder(this, sessionToken)
.buildAsync();
mediaControllerFuture.addListener(() -> {
try {
mediaController = mediaControllerFuture.get();
addActivityContext(
this.getIntent().getExtras(),
this.getClass().getName()
);
playbackViewModel.setMediaController(mediaController);
} catch (Exception e) {
Log.e(TAG, "Error connecting to audio playback service", e);
}
}, ContextCompat.getMainExecutor(this));
}

private void addActivityContext(Bundle extras, String activityClassName) {
if (mediaController == null) return;

Bundle commandArgs = new Bundle();
commandArgs.putString("activity_class", activityClassName);
if (extras != null) {
commandArgs.putAll(extras);
}

SessionCommand updateContextCommand =
new SessionCommand("UPDATE_ACTIVITY_CONTEXT", Bundle.EMPTY);

mediaController.sendCustomCommand(updateContextCommand, commandArgs);
}

private boolean isGlobalGallery() {
return contactId==0 && chatId==0;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@
import com.b44t.messenger.DcMsg;
import com.codewaves.stickyheadergrid.StickyHeaderGridAdapter;

import org.thoughtcrime.securesms.components.AudioView;
import org.thoughtcrime.securesms.components.DocumentView;
import org.thoughtcrime.securesms.components.WebxdcView;
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
import org.thoughtcrime.securesms.components.audioplay.AudioView;
import org.thoughtcrime.securesms.database.loaders.BucketedThreadMediaLoader.BucketedThreadMedia;
import org.thoughtcrime.securesms.mms.AudioSlide;
import org.thoughtcrime.securesms.mms.DocumentSlide;
Expand All @@ -31,7 +32,8 @@ class AllMediaDocumentsAdapter extends StickyHeaderGridAdapter {
private final ItemClickListener itemClickListener;
private final Set<DcMsg> selected;

private BucketedThreadMedia media;
private BucketedThreadMedia media;
private AudioPlaybackViewModel playbackViewModel;

private static class ViewHolder extends StickyHeaderGridAdapter.ItemViewHolder {
private final DocumentView documentView;
Expand Down Expand Up @@ -71,6 +73,10 @@ public void setMedia(BucketedThreadMedia media) {
this.media = media;
}

public void setPlaybackViewModel(AudioPlaybackViewModel playbackViewModel) {
this.playbackViewModel = playbackViewModel;
}

@Override
public StickyHeaderGridAdapter.HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent, int headerType) {
return new HeaderHolder(LayoutInflater.from(context).inflate(R.layout.contact_selection_list_divider, parent, false));
Expand All @@ -97,6 +103,7 @@ public void onBindItemViewHolder(ItemViewHolder itemViewHolder, int section, int
viewHolder.webxdcView.setVisibility(View.GONE);

viewHolder.audioView.setVisibility(View.VISIBLE);
viewHolder.audioView.setPlaybackViewModel(playbackViewModel);
viewHolder.audioView.setAudio((AudioSlide)slide, dcMsg.getDuration());
viewHolder.audioView.setOnClickListener(view -> itemClickListener.onMediaClicked(dcMsg));
viewHolder.audioView.setOnLongClickListener(view -> { itemClickListener.onMediaLongClicked(dcMsg); return true; });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.view.ActionMode;
import androidx.lifecycle.ViewModelProvider;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;
import androidx.recyclerview.widget.RecyclerView;
Expand All @@ -25,6 +26,7 @@
import com.b44t.messenger.DcMsg;
import com.codewaves.stickyheadergrid.StickyHeaderGridLayoutManager;

import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
import org.thoughtcrime.securesms.connect.DcEventCenter;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.database.loaders.BucketedThreadMediaLoader;
Expand Down Expand Up @@ -72,9 +74,11 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
// add padding to avoid content hidden behind system bars
ViewUtil.applyWindowInsets(recyclerView, true, false, true, true);

this.recyclerView.setAdapter(new AllMediaDocumentsAdapter(getContext(),
new BucketedThreadMediaLoader.BucketedThreadMedia(getContext()),
this));
AllMediaDocumentsAdapter adapter = new AllMediaDocumentsAdapter(getContext(),
new BucketedThreadMediaLoader.BucketedThreadMedia(getContext()),
this);
this.recyclerView.setAdapter(adapter);
adapter.setPlaybackViewModel(new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class));
this.recyclerView.setLayoutManager(gridManager);
this.recyclerView.setHasFixedSize(true);

Expand Down Expand Up @@ -239,12 +243,13 @@ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem menuItem) {
int itemId = menuItem.getItemId();
AudioPlaybackViewModel playbackViewModel = new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class);
if (itemId == R.id.details) {
handleDisplayDetails(getSelectedMessageRecord(getListAdapter().getSelectedMedia()));
mode.finish();
return true;
} else if (itemId == R.id.delete) {
handleDeleteMessages(chatId, getListAdapter().getSelectedMedia());
handleDeleteMessages(chatId, getListAdapter().getSelectedMedia(), playbackViewModel::stopByIds, playbackViewModel::stopByIds);
mode.finish();
return true;
} else if (itemId == R.id.share) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,11 @@ public BaseConversationItem(Context context, AttributeSet attrs) {
this.rpc = DcHelper.getRpc(context);
}

protected void bind(@NonNull DcMsg messageRecord,
@NonNull DcChat dcChat,
@NonNull Set<DcMsg> batchSelected,
boolean pulseHighlight,
@NonNull Recipient conversationRecipient)
protected void bindPartial(@NonNull DcMsg messageRecord,
@NonNull DcChat dcChat,
@NonNull Set<DcMsg> batchSelected,
boolean pulseHighlight,
@NonNull Recipient conversationRecipient)
{
this.messageRecord = messageRecord;
this.dcChat = dcChat;
Expand Down Expand Up @@ -126,6 +126,8 @@ protected class ClickListener implements View.OnClickListener {

public void onClick(View v) {
if (!shouldInterceptClicks(messageRecord) && parent != null) {
// The click workaround on ConversationItem shall be revised.
// In fact, it is probably better rethinking accessibility approach for the items.
if (batchSelected.isEmpty() && Util.isTouchExplorationEnabled(context)) {
BaseConversationItem.this.onAccessibilityClick();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcMsg;

import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
import org.thoughtcrime.securesms.components.audioplay.AudioView;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;

Expand All @@ -17,7 +19,9 @@ void bind(@NonNull DcMsg messageRecord,
@NonNull GlideRequests glideRequests,
@NonNull Set<DcMsg> batchSelected,
@NonNull Recipient recipients,
boolean pulseHighlight);
boolean pulseHighlight,
@Nullable AudioPlaybackViewModel playbackViewModel,
AudioView.OnActionListener audioPlayPauseListener);

DcMsg getMessageRecord();

Expand Down
Loading
Loading