Compare commits

...

29 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] cb84e9fe3c Tighten body-tag auto-direction injection edge handling
Agent-Logs-Url: https://github.com/ArcaneChat/android/sessions/7f55ea69-ba6c-40ad-a72e-3080ac96b847

Co-authored-by: adbenitez <24558636+adbenitez@users.noreply.github.com>
2026-05-08 13:12:30 +00:00
copilot-swe-agent[bot] 02812b0f3b Refine auto-direction injection without regex in FullMsgActivity
Agent-Logs-Url: https://github.com/ArcaneChat/android/sessions/7f55ea69-ba6c-40ad-a72e-3080ac96b847

Co-authored-by: adbenitez <24558636+adbenitez@users.noreply.github.com>
2026-05-08 13:10:15 +00:00
copilot-swe-agent[bot] 7f625d6e8d Add auto text direction in FullMsgActivity HTML rendering
Agent-Logs-Url: https://github.com/ArcaneChat/android/sessions/7f55ea69-ba6c-40ad-a72e-3080ac96b847

Co-authored-by: adbenitez <24558636+adbenitez@users.noreply.github.com>
2026-05-08 13:08:05 +00:00
adbenitez dcaa19b29d Merge remote-tracking branch 'upstream/main' 2026-05-05 21:01:48 +02:00
adb 11845da65f Merge pull request #4407 from deltachat/adb/issue-4406
add "mark all as read" to account menu
2026-05-05 18:03:46 +02:00
adbenitez 82262cb166 update changelog 2026-05-05 15:41:55 +02:00
adbenitez e85ae5247a add "mark all as read" to account menu 2026-05-05 15:40:06 +02:00
adb 40cdeee47a Merge pull request #4404 from deltachat/adb/issue-4396
don't set draft if user can't send
2026-05-05 15:09:37 +02:00
adb 5040fb4634 Merge branch 'main' into adb/issue-4396 2026-05-04 22:04:51 +02:00
adb 4313e8824b Merge pull request #4395 from deltachat/adb/issue-4200
allow to open message links via actions in TalkBack menu
2026-05-04 20:42:37 +02:00
B. Petersen 102c3fd82a make strings as deprecated 2026-05-04 20:14:19 +02:00
adbenitez dc31eee4bc Merge branch 'adb/issue-4200' of https://github.com/deltachat/deltachat-android into adb/issue-4200 2026-05-04 20:14:06 +02:00
adbenitez 875f0e5c86 remove unnecessary check 2026-05-04 20:13:37 +02:00
wchen342 2b36e2dc26 Hide webxdc attach button for channels (#4402) 2026-05-04 20:02:37 +02:00
adbenitez 6a9c3ab8ca update changelog 2026-05-04 19:57:10 +02:00
adbenitez 2ab544b18e don't set draft if user can't send 2026-05-04 19:55:13 +02:00
adbenitez 2630e22421 Merge remote-tracking branch 'upstream/main' 2026-05-04 19:20:04 +02:00
wchen342 1b7e32ab44 Get channels out of experimental (#4403)
* Get channels out of experimental

* Update CHANGELOG.md

Co-authored-by: adb <adb@merlinux.eu>
2026-05-04 18:28:51 +02:00
adb 8436909977 Merge branch 'main' into adb/issue-4200 2026-05-01 15:51:40 +02:00
adb 9dd44c24b3 Merge pull request #4394 from deltachat/adb/open-map-on-loc-streaming-enabled
open map if user clicks the "Location streaming enabled by Foo/you" message
2026-05-01 15:47:14 +02:00
adbenitez 5d0e85b30d update changelog 2026-05-01 15:41:06 +02:00
adbenitez 5193dfa412 allow to open message links via actions in TalkBack menu 2026-05-01 15:38:06 +02:00
adbenitez 96490e23f8 update changelog 2026-04-30 17:59:31 +02:00
adbenitez 7959b07d45 open map if user clicks the "Location streaming enabled by Foo/you" message 2026-04-30 17:57:35 +02:00
link2xt c4ae7927ad Setup zizmor workflow to check GitHub Actions workflows 2026-04-29 17:13:09 +00:00
wchen342 ba0515ebc5 Allow voice messages to be played continuously in a chat (#4376)
* Allow voice messages to be played continuously in a chat

---------

Co-authored-by: adb <adb@merlinux.eu>
2026-04-29 17:24:10 +02:00
adb 349bab9510 Merge pull request #4392 from deltachat/adb/issue-4391
change sticker sort order
2026-04-29 17:05:26 +02:00
adbenitez e26f803d1f update changelog 2026-04-29 17:01:36 +02:00
adbenitez 67d2c89bcc change sticker sort order 2026-04-29 15:58:11 +02:00
26 changed files with 389 additions and 76 deletions
+7 -1
View File
@@ -4,14 +4,20 @@ on:
workflows: ["Upload Preview APK"]
types: [completed]
permissions: {}
jobs:
artifacts-url-comments:
name: add artifact links to pull request
permissions:
pull-requests: write
actions: read
contents: read
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: add artifact links to pull request
uses: tonyhallett/artifacts-url-comments@v1.1.0
uses: tonyhallett/artifacts-url-comments@0965ff1a7ae03c5c1644d3c30f956effea4e05ef # v1.1.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
+6 -3
View File
@@ -10,6 +10,8 @@ on:
- 'jni/dc_wrapper.c'
- 'scripts/ndk-make.sh'
permissions: {}
jobs:
build:
runs-on: ubuntu-latest
@@ -17,14 +19,15 @@ jobs:
- uses: actions/checkout@v5
with:
submodules: recursive
persist-credentials: false
- uses: android-actions/setup-android@v3
- uses: nttld/setup-ndk@v1
- uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1
- uses: nttld/setup-ndk@ed92fe6cadad69be94a966a7ee3271275e62f779 # v1.6.0
id: setup-ndk
with:
ndk-version: r27
- uses: Swatinem/rust-cache@v2
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
workspaces: jni/deltachat-core-rust
+5 -1
View File
@@ -5,6 +5,8 @@ on:
branches: [main]
pull_request:
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
@@ -17,6 +19,8 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v5
with:
persist-credentials: false
- uses: actions/setup-java@v5
with:
java-version: 17
@@ -30,6 +34,6 @@ jobs:
restore-keys: |
${{ runner.os }}-gradle-
- name: Validate Gradle Wrapper
uses: gradle/actions/wrapper-validation@v4
uses: gradle/actions/wrapper-validation@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
- name: Check formatting
run: ./gradlew spotlessCheck
+7 -4
View File
@@ -6,6 +6,8 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
permissions: {}
jobs:
build:
name: Upload Preview APK
@@ -14,6 +16,7 @@ jobs:
- uses: actions/checkout@v5
with:
submodules: recursive
persist-credentials: false
- name: Validate Fastlane Metadata
uses: ashutoshgngwr/validate-fastlane-supply-metadata@v2
@@ -33,15 +36,15 @@ jobs:
${{ runner.os }}-gradle-
- name: Validate Gradle Wrapper
uses: gradle/actions/wrapper-validation@v4
uses: gradle/actions/wrapper-validation@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
- uses: android-actions/setup-android@v3
- uses: nttld/setup-ndk@v1
- uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1
- uses: nttld/setup-ndk@ed92fe6cadad69be94a966a7ee3271275e62f779 # v1.6.0
id: setup-ndk
with:
ndk-version: r27
- uses: Swatinem/rust-cache@v2
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
workspaces: jni/deltachat-core-rust
+26
View File
@@ -0,0 +1,26 @@
name: GitHub Actions Security Analysis with zizmor
on:
push:
branches: ["main"]
pull_request:
branches: ["**"]
permissions: {}
jobs:
zizmor:
name: Run zizmor
runs-on: ubuntu-latest
permissions:
security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files.
contents: read
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Run zizmor
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
+6
View File
@@ -0,0 +1,6 @@
rules:
unpinned-uses:
config:
policies:
actions/*: ref-pin
dependabot/*: ref-pin
+7
View File
@@ -4,11 +4,18 @@
* Better incoming call system integration
* Calls are not experimental anymore and don't need to be manually enabled
* Channels are no longer experimental and are available by default
* Display a permanent notification when doing location streaming and get rid of dangerous "Access Location in Background" permission
* Autoplay all voice messages in a chat
* Allow to share location for 24 hours
* Allow mini-apps to play audio without user interaction
* Mark chats as unread (long tap a chat and select the corresponding option from the three-dot-menu)
* Add "Mark all as read" option to profile menu in the profile switcher
* Fix process of upgrading from a very old version of the app
* Show more recent added stickers at the top of the sticker picker
* Allow to open links in messages via actions in TalkBack menu
* Allow to open map if user clicks "Location streaming enabled" system message
* Fix: do not accidentally set draft in chats that don't allow sending messages
## v2.49.0
2026-04
@@ -27,6 +27,7 @@ import com.google.android.material.tabs.TabLayout;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList;
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
import org.thoughtcrime.securesms.components.audioplay.ChatAudioQueueProvider;
import org.thoughtcrime.securesms.connect.DcEventCenter;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.service.AudioPlaybackService;
@@ -107,7 +108,9 @@ public class AllMediaActivity extends PassphraseRequiredActionBarActivity
eventCenter.addObserver(DcContext.DC_EVENT_CHAT_MODIFIED, this);
eventCenter.addObserver(DcContext.DC_EVENT_CONTACTS_CHANGED, this);
int accountId = DcHelper.getAccounts(this).getSelectedAccount().getAccountId();
playbackViewModel = new ViewModelProvider(this).get(AudioPlaybackViewModel.class);
playbackViewModel.setQueueProvider(new ChatAudioQueueProvider(this, chatId, accountId));
initializeMediaController();
}
@@ -119,6 +122,7 @@ public class AllMediaActivity extends PassphraseRequiredActionBarActivity
mediaController = null;
playbackViewModel.setMediaController(null);
}
playbackViewModel.setQueueProvider(null);
super.onDestroy();
}
@@ -101,6 +101,7 @@ import org.thoughtcrime.securesms.components.ScaleStableImageView;
import org.thoughtcrime.securesms.components.SendButton;
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
import org.thoughtcrime.securesms.components.audioplay.AudioView;
import org.thoughtcrime.securesms.components.audioplay.ChatAudioQueueProvider;
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
import org.thoughtcrime.securesms.connect.AccountManager;
import org.thoughtcrime.securesms.connect.DcEventCenter;
@@ -223,7 +224,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
initializeViews();
initializeResources();
int accountId = DcHelper.getAccounts(this).getSelectedAccount().getAccountId();
playbackViewModel = new ViewModelProvider(this).get(AudioPlaybackViewModel.class);
playbackViewModel.setQueueProvider(new ChatAudioQueueProvider(this, chatId, accountId));
initializeMediaController();
initializeSecurity(false, isDefaultSms)
@@ -413,6 +416,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
mediaController = null;
playbackViewModel.setMediaController(null);
}
playbackViewModel.setQueueProvider(null);
super.onDestroy();
}
@@ -672,8 +676,14 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
public void setDraftText(String txt) {
composeText.setText(txt);
composeText.setSelection(composeText.getText().length());
try {
if (rpc.canSend(rpc.getSelectedAccountId(), chatId)) {
composeText.setText(txt);
composeText.setSelection(composeText.getText().length());
}
} catch (RpcException e) {
Log.e(TAG, "Rpc error", e);
}
}
public void hideSoftKeyboard() {
@@ -1119,6 +1129,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
if (chatId == DcChat.DC_CHAT_NO_CHAT)
throw new IllegalStateException("can't display a conversation for no chat.");
dcChat = DcHelper.getContext(context).getChat(chatId);
attachmentTypeSelector = null;
recipient = new Recipient(this, dcChat);
glideRequests = GlideApp.with(this);
@@ -1267,6 +1278,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
DcContext dcContext = DcHelper.getContext(context);
final int currentChatId = dcChat.getId();
final boolean canSend = dcChat.canSend();
Util.runOnAnyBackgroundThread(
() -> {
DcMsg msg = null;
@@ -1389,7 +1401,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
}
} else {
dcContext.setDraft(currentChatId, msg);
// set or clear draft if user can't send in chat since they can't delete it otherwise
dcContext.setDraft(currentChatId, canSend ? msg : null);
}
future.set(currentChatId);
});
@@ -944,6 +944,8 @@ public class ConversationFragment extends MessageSelectorFragment {
WebxdcActivity.openWebxdcActivity(
getContext(), messageRecord.getParent(), messageRecord.getWebxdcHref());
}
} else if (messageRecord.getInfoType() == DcMsg.DC_INFO_LOCATIONSTREAMING_ENABLED) {
WebxdcActivity.openMaps(getContext(), (int) chatId);
} else if (messageRecord.getInfoType() == DcMsg.DC_INFO_CHAT_DESCRIPTION_CHANGED) {
Intent intent = new Intent(getContext(), ProfileActivity.class);
intent.putExtra(ProfileActivity.CHAT_ID_EXTRA, (int) chatId);
@@ -24,6 +24,7 @@ import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.os.Build;
import android.text.Spannable;
import android.text.Spanned;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
@@ -36,6 +37,7 @@ import androidx.annotation.DimenRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.core.view.ViewCompat;
import chat.delta.rpc.RpcException;
import chat.delta.rpc.types.CallInfo;
import chat.delta.rpc.types.CallState;
@@ -44,6 +46,7 @@ import chat.delta.rpc.types.VcardContact;
import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContact;
import com.b44t.messenger.DcMsg;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import org.thoughtcrime.securesms.calls.CallCoordinator;
@@ -72,6 +75,7 @@ import org.thoughtcrime.securesms.mms.VcardSlide;
import org.thoughtcrime.securesms.reactions.ReactionsConversationView;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.Linkifier;
import org.thoughtcrime.securesms.util.LongClickCopySpan;
import org.thoughtcrime.securesms.util.LongClickMovementMethod;
import org.thoughtcrime.securesms.util.MarkdownUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
@@ -120,6 +124,9 @@ public class ConversationItem extends BaseConversationItem {
private Stub<CallItemView> callViewStub;
private @Nullable EventListener eventListener;
// IDs of accessibility actions registered via ViewCompat.addAccessibilityAction
private final List<Integer> linkActionIds = new ArrayList<>();
private int measureCalls;
private int incomingBubbleColor;
@@ -424,6 +431,12 @@ public class ConversationItem extends BaseConversationItem {
bodyText.setClickable(false);
bodyText.setFocusable(false);
// Remove any link actions registered for the previous message binding
for (int id : linkActionIds) {
ViewCompat.removeAccessibilityAction(this, id);
}
linkActionIds.clear();
String subject = messageRecord.getSubject();
String text = messageRecord.getText();
@@ -433,12 +446,33 @@ public class ConversationItem extends BaseConversationItem {
if (messageRecord.getType() == DcMsg.DC_MSG_CALL || text.isEmpty()) {
bodyText.setVisibility(View.GONE);
} else {
Spannable spannable = (Spannable) MarkdownUtil.toMarkdown(context, text);
Spannable spannable = MarkdownUtil.toMarkdown(context, text);
if (batchSelected.isEmpty()) {
spannable = Linkifier.linkify(spannable);
Linkifier.linkify(spannable);
}
bodyText.setText(spannable);
bodyText.setVisibility(View.VISIBLE);
// Register a TalkBack "Actions" entry for each link in the message
Spanned spanned = (Spanned) spannable;
final TextView tv = bodyText;
for (LongClickCopySpan span :
spanned.getSpans(0, spanned.length(), LongClickCopySpan.class)) {
int start = spanned.getSpanStart(span);
int end = spanned.getSpanEnd(span);
if (start >= 0 && end > start && end <= spanned.length()) {
String linkText = spanned.subSequence(start, end).toString();
String label = context.getString(R.string.accessibility_link_action, linkText);
linkActionIds.add(
ViewCompat.addAccessibilityAction(
this,
label,
(v, args) -> {
span.onClick(tv);
return true;
}));
}
}
}
int downloadState = messageRecord.getDownloadState();
@@ -15,6 +15,7 @@ import chat.delta.rpc.types.HttpResponse;
import com.b44t.messenger.DcContext;
import java.io.ByteArrayInputStream;
import java.lang.ref.WeakReference;
import java.util.Locale;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.JsonUtils;
@@ -120,6 +121,7 @@ public class FullMsgActivity extends WebViewActivity {
try {
FullMsgActivity activity = activityReference.get();
String html = activity.dcContext.getMsgHtml(activity.msgId);
html = withAutoTextDirection(html);
activity.runOnUiThread(
() -> {
@@ -141,6 +143,31 @@ public class FullMsgActivity extends WebViewActivity {
});
}
private static String withAutoTextDirection(String html) {
if (html == null || html.isEmpty()) {
return html;
}
String lowercaseHtml = html.toLowerCase(Locale.US);
int bodyTagStart = lowercaseHtml.indexOf("<body");
if (bodyTagStart >= 0) {
int bodyTagEnd = lowercaseHtml.indexOf(">", bodyTagStart);
if (bodyTagEnd < 0) {
return html;
}
String bodyTag = html.substring(bodyTagStart, bodyTagEnd + 1);
String lowercaseBodyTag = lowercaseHtml.substring(bodyTagStart, bodyTagEnd + 1);
if (lowercaseBodyTag.contains("dir=")) {
return html;
}
if (lowercaseBodyTag.endsWith("/>")) {
return html;
}
String updatedBodyTag = bodyTag.substring(0, bodyTag.length() - 1) + " dir=\"auto\">";
return html.substring(0, bodyTagStart) + updatedBodyTag + html.substring(bodyTagEnd + 1);
}
return "<div dir=\"auto\">" + html + "</div>";
}
@Override
protected void onPause() {
super.onPause();
@@ -157,6 +157,12 @@ public class AccountSelectionListFragment extends DialogFragment
onSetTag(accountId);
} else if (itemId == R.id.menu_move_to_top) {
onMoveToTop(accountId);
} else if (itemId == R.id.menu_mark_all_as_read) {
try {
DcHelper.getRpc(requireActivity()).marknoticedAllChats(accountId);
} catch (RpcException e) {
Log.e(TAG, "Error calling rpc.marknoticedAllChats()", e);
}
}
}
@@ -92,6 +92,10 @@ public class AttachmentTypeSelector extends PopupWindow {
ViewUtil.findById(layout, R.id.location_linear_layout).setVisibility(View.GONE);
}
if (DcHelper.getContext(context).getChat(chatId).isOutBroadcast()) {
ViewUtil.findById(layout, R.id.webxdc_linear_layout).setVisibility(View.GONE);
}
setLocationButtonImage(context);
setContentView(layout);
@@ -6,15 +6,19 @@ import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import androidx.media3.common.MediaItem;
import androidx.media3.common.PlaybackException;
import androidx.media3.common.Player;
import androidx.media3.session.MediaController;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
@@ -34,6 +38,8 @@ public class AudioPlaybackViewModel extends ViewModel {
private final ExecutorService extractionExecutor = Executors.newFixedThreadPool(2);
private @Nullable MediaController mediaController;
private @Nullable ChatAudioQueueProvider queueProvider;
private @Nullable Player.Listener playerListener;
private final Handler handler;
private boolean isUserSeeking = false;
@@ -47,6 +53,11 @@ public class AudioPlaybackViewModel extends ViewModel {
}
public void setMediaController(@Nullable MediaController controller) {
if (this.mediaController != null && playerListener != null) {
this.mediaController.removeListener(playerListener);
}
playerListener = null;
this.mediaController = controller;
if (mediaController != null && mediaController.isPlaying()) {
startUpdateProgress();
@@ -59,30 +70,41 @@ public class AudioPlaybackViewModel extends ViewModel {
public void loadAudioAndPlay(int msgId, Uri audioUri) {
if (mediaController == null) return;
// Set media item if we have a different audio.
if (isDifferentAudio(msgId, audioUri)) {
updateState(msgId, audioUri, AudioPlaybackState.PlaybackStatus.LOADING, 0, 0);
String mediaId = String.valueOf(msgId);
MediaItem mediaItem =
new MediaItem.Builder().setMediaId(String.valueOf(msgId)).setUri(audioUri).build();
mediaController.setMediaItem(mediaItem);
mediaController.prepare();
MediaItem current = mediaController.getCurrentMediaItem();
if (current != null && mediaId.equals(current.mediaId)) {
mediaController.play();
return;
}
play(msgId, audioUri);
updateState(msgId, audioUri, AudioPlaybackState.PlaybackStatus.LOADING, 0, 0);
List<MediaItem> items = null;
int startIndex = -1;
if (queueProvider != null) {
items = queueProvider.buildAudioQueue();
startIndex = indexOfMediaId(items, mediaId);
}
if (startIndex < 0) {
items =
Collections.singletonList(
new MediaItem.Builder().setMediaId(mediaId).setUri(audioUri).build());
startIndex = 0;
}
mediaController.setMediaItems(items, startIndex, 0);
mediaController.prepare();
mediaController.play();
}
private boolean isSameAudio(int msgId, Uri audioUri) {
return !isDifferentAudio(msgId, audioUri);
}
private boolean isDifferentAudio(int msgId, Uri audioUri) {
AudioPlaybackState currentState = playbackState.getValue();
return currentState != null
&& (msgId != currentState.getMsgId()
|| currentState.getAudioUri() == null
|| currentState.getAudioUri() != null && !currentState.getAudioUri().equals(audioUri));
private static int indexOfMediaId(List<MediaItem> items, String mediaId) {
for (int i = 0; i < items.size(); i++) {
if (mediaId.equals(items.get(i).mediaId)) return i;
}
return -1;
}
public LiveData<Map<Integer, Long>> getDurations() {
@@ -138,46 +160,72 @@ public class AudioPlaybackViewModel extends ViewModel {
}
}
public void pause(int msgId, Uri audioUri) {
if (mediaController != null && isSameAudio(msgId, audioUri)) {
public void pause(int msgId) {
if (isCurrentItem(msgId)) {
mediaController.pause();
}
}
public void play(int msgId, Uri audioUri) {
if (mediaController != null && isSameAudio(msgId, audioUri)) {
public void play(int msgId) {
if (isCurrentItem(msgId)) {
mediaController.play();
}
}
public void seekTo(long position, int msgId, Uri audioUri) {
if (mediaController != null && isSameAudio(msgId, audioUri)) {
public void seekTo(long position, int msgId) {
if (isCurrentItem(msgId)) {
mediaController.seekTo(position);
}
}
public void stop(int msgId, Uri audioUri) {
if (mediaController != null && isSameAudio(msgId, audioUri)) {
public void stop(int msgId) {
if (isCurrentItem(msgId)) {
mediaController.stop();
mediaController.clearMediaItems();
stopUpdateProgress();
playbackState.setValue(AudioPlaybackState.idle());
}
}
private boolean isCurrentItem(int msgId) {
if (mediaController == null) return false;
MediaItem current = mediaController.getCurrentMediaItem();
return current != null && String.valueOf(msgId).equals(current.mediaId);
}
public void stopNonMessageAudioPlayback() {
stopByIds(NON_MESSAGE_AUDIO_MSG_ID);
}
// A special method for deleting message, where we only use message Ids
public void stopByIds(int... msgIds) {
AudioPlaybackState currentState = playbackState.getValue();
if (mediaController == null) return;
if (mediaController != null && currentState != null) {
AudioPlaybackState currentState = playbackState.getValue();
boolean stoppedCurrent = false;
if (currentState != null) {
for (int msgId : msgIds) {
if (msgId == currentState.getMsgId()) {
mediaController.stop();
mediaController.clearMediaItems();
stopUpdateProgress();
playbackState.setValue(AudioPlaybackState.idle());
stoppedCurrent = true;
break;
}
}
}
if (!stoppedCurrent) {
Set<String> deletedMediaIds = new HashSet<>();
for (int msgId : msgIds) {
deletedMediaIds.add(String.valueOf(msgId));
}
for (int i = mediaController.getMediaItemCount() - 1; i >= 0; i--) {
MediaItem item = mediaController.getMediaItemAt(i);
if (deletedMediaIds.contains(item.mediaId)) {
mediaController.removeMediaItem(i);
}
}
}
@@ -191,10 +239,10 @@ public class AudioPlaybackViewModel extends ViewModel {
private void setupPlayerListener() {
if (mediaController == null) return;
mediaController.addListener(
playerListener =
new Player.Listener() {
@Override
public void onEvents(Player player, Player.Events events) {
public void onEvents(@NonNull Player player, @NonNull Player.Events events) {
if (events.containsAny(Player.EVENT_IS_PLAYING_CHANGED)) {
if (player.isPlaying()) {
startUpdateProgress();
@@ -203,20 +251,40 @@ public class AudioPlaybackViewModel extends ViewModel {
}
updateCurrentState(false);
}
if (events.containsAny(Player.EVENT_MEDIA_ITEM_TRANSITION)) {
updateCurrentState(true);
}
if (events.containsAny(Player.EVENT_PLAYBACK_STATE_CHANGED)) {
if (player.getPlaybackState() == Player.STATE_READY) {
updateCurrentState(false);
} else if (player.getPlaybackState() == Player.STATE_ENDED) {
// This is to prevent automatically playing after the audio
// has been play to the end once, then user dragged the seek bar again
} else if (player.getPlaybackState() == Player.STATE_ENDED
&& !player.hasNextMediaItem()) {
mediaController.setPlayWhenReady(false);
}
}
if (events.containsAny(Player.EVENT_PLAYER_ERROR)) {
}
@Override
public void onPlayerError(@NonNull PlaybackException error) {
Log.w(
TAG,
"Playback error on msgId="
+ (mediaController.getCurrentMediaItem() != null
? mediaController.getCurrentMediaItem().mediaId
: "null"),
error);
if (mediaController.hasNextMediaItem()) {
mediaController.seekToNextMediaItem();
mediaController.prepare();
mediaController.play();
} else {
updateCurrentAudioState(AudioPlaybackState.PlaybackStatus.ERROR, 0, 0);
mediaController.clearMediaItems();
}
}
});
};
mediaController.addListener(playerListener);
}
private void updateCurrentState(boolean queryPlaying) {
@@ -285,6 +353,11 @@ public class AudioPlaybackViewModel extends ViewModel {
}
}
// Playing Queue
public void setQueueProvider(@Nullable ChatAudioQueueProvider provider) {
this.queueProvider = provider;
}
// Progress tracking
private final Runnable progressRunnable =
new Runnable() {
@@ -109,12 +109,12 @@ public class AudioView extends FrameLayout {
AudioPlaybackState state = viewModel.getPlaybackState().getValue();
if (state != null && msgId == state.getMsgId() && audioUri.equals(state.getAudioUri())) {
if (state != null && msgId == state.getMsgId()) {
// Same audio
if (state.getStatus() == AudioPlaybackState.PlaybackStatus.PLAYING) {
viewModel.pause(msgId, audioUri);
viewModel.pause(msgId);
} else {
viewModel.play(msgId, audioUri);
viewModel.play(msgId);
}
} else {
// Different audio
@@ -145,7 +145,7 @@ public class AudioView extends FrameLayout {
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
viewModel.setUserSeeking(false);
viewModel.seekTo(seekBar.getProgress(), msgId, audioUri);
viewModel.seekTo(seekBar.getProgress(), msgId);
}
});
@@ -305,7 +305,7 @@ public class AudioView extends FrameLayout {
if (audioUri == null || state == null) return;
// Check if this state is about this message
boolean isThisMessage = msgId == state.getMsgId() && audioUri.equals(state.getAudioUri());
boolean isThisMessage = msgId == state.getMsgId();
if (isThisMessage) {
updateUIForPlaybackState(state);
@@ -340,13 +340,9 @@ public class AudioView extends FrameLayout {
private void onDurationsChanged(Map<Integer, Long> durations) {
AudioPlaybackState state = viewModel.getPlaybackState().getValue();
// When there is no playback happening, msgId can be -1 and audioUri is null
if (state != null
&& msgId >= 0
&& msgId == state.getMsgId()
&& audioUri != null
&& audioUri.equals(state.getAudioUri())) {
return; // Is playing this message
// When there is no playback happening, msgId can be -1
if (state != null && msgId >= 0 && msgId == state.getMsgId()) {
return;
}
Long duration = durations.get(msgId);
@@ -0,0 +1,41 @@
package org.thoughtcrime.securesms.components.audioplay;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.media3.common.MediaItem;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcMsg;
import java.util.ArrayList;
import java.util.List;
import org.thoughtcrime.securesms.connect.DcHelper;
public class ChatAudioQueueProvider {
private final Context context;
private final int chatId;
private final int accountId;
public ChatAudioQueueProvider(@NonNull Context context, int chatId, int accountId) {
this.context = context.getApplicationContext();
this.chatId = chatId;
this.accountId = accountId;
}
@NonNull
public List<MediaItem> buildAudioQueue() {
DcContext dcContext = DcHelper.getContext(context);
int[] msgIds = dcContext.getChatMedia(chatId, DcMsg.DC_MSG_AUDIO, DcMsg.DC_MSG_VOICE, 0);
List<MediaItem> items = new ArrayList<>(msgIds.length);
for (int msgId : msgIds) {
String id = String.valueOf(msgId);
items.add(
new MediaItem.Builder()
.setMediaId(id)
.setUri(Uri.parse("dcmsg://" + accountId + "/" + id))
.build());
}
return items;
}
}
@@ -71,7 +71,7 @@ public class StickerPickerView extends RecyclerView {
}
// Sort stickers just to provide consistent order
Collections.sort(stickerFiles, (f1, f2) -> f1.getName().compareToIgnoreCase(f2.getName()));
Collections.sort(stickerFiles, (f1, f2) -> f2.getName().compareToIgnoreCase(f1.getName()));
return stickerFiles;
}
@@ -5,7 +5,6 @@ import androidx.annotation.NonNull;
import com.b44t.messenger.DcContact;
import com.b44t.messenger.DcContext;
import org.thoughtcrime.securesms.util.AsyncLoader;
import org.thoughtcrime.securesms.util.Prefs;
import org.thoughtcrime.securesms.util.Util;
public class DcContactsLoader extends AsyncLoader<DcContactsLoader.Ret> {
@@ -53,10 +52,7 @@ public class DcContactsLoader extends AsyncLoader<DcContactsLoader.Ret> {
}
if (query == null && addCreateGroupLinks) {
additional_items = Util.appendInt(additional_items, DcContact.DC_CONTACT_ID_NEW_GROUP);
final boolean broadcastsEnabled = Prefs.isNewBroadcastAvailable(getContext());
if (broadcastsEnabled)
additional_items = Util.appendInt(additional_items, DcContact.DC_CONTACT_ID_NEW_BROADCAST);
additional_items = Util.appendInt(additional_items, DcContact.DC_CONTACT_ID_NEW_BROADCAST);
if (!dcContext.isChatmail()) {
additional_items =
@@ -36,7 +36,7 @@ public class PlatformLocationSource implements LocationSource {
registered = requestProvider(context, LocationManager.FUSED_PROVIDER, callback);
}
// Fall back (or complement) with individual providers
// Fall back with individual providers
if (!registered) {
requestProvider(context, LocationManager.GPS_PROVIDER, callback);
requestProvider(context, LocationManager.NETWORK_PROVIDER, callback);
@@ -283,7 +283,7 @@ public class AttachmentManager {
removableMediaView.display(audioView, false);
removableMediaView.addRemoveClickListener(
v -> {
playbackViewModel.stop(audioView.getMsgId(), audioView.getAudioUri());
playbackViewModel.stop(audioView.getMsgId());
});
result.set(true);
} else if (slide.isVcard()) {
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.service;
import android.app.PendingIntent;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import androidx.annotation.NonNull;
@@ -10,15 +11,24 @@ import androidx.annotation.OptIn;
import androidx.media3.common.AudioAttributes;
import androidx.media3.common.C;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.datasource.DefaultDataSource;
import androidx.media3.datasource.ResolvingDataSource;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
import androidx.media3.exoplayer.source.MediaSource;
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.b44t.messenger.DcContext;
import com.b44t.messenger.DcMsg;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import java.io.IOException;
import org.thoughtcrime.securesms.ConversationListActivity;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.mms.AudioSlide;
public class AudioPlaybackService extends MediaSessionService {
@@ -27,6 +37,7 @@ public class AudioPlaybackService extends MediaSessionService {
private ExoPlayer player;
private MediaSession session;
@OptIn(markerClass = UnstableApi.class)
@Override
public void onCreate() {
super.onCreate();
@@ -37,10 +48,47 @@ public class AudioPlaybackService extends MediaSessionService {
.setContentType(C.AUDIO_CONTENT_TYPE_SPEECH)
.build();
ResolvingDataSource.Factory dataSourceFactory =
new ResolvingDataSource.Factory(
new DefaultDataSource.Factory(this),
dataSpec -> {
Uri uri = dataSpec.uri;
if (!"dcmsg".equals(uri.getScheme())) {
return dataSpec;
}
String host = uri.getHost();
String segment = uri.getLastPathSegment();
int accountId = 0;
int msgId = 0;
if (host != null && segment != null) {
try {
accountId = Integer.parseInt(host);
msgId = Integer.parseInt(segment);
} catch (NumberFormatException ignored) {
}
}
if (accountId <= 0 || msgId <= 0) {
throw new IOException("Invalid dcmsg uri: " + uri);
}
DcContext dcContext = DcHelper.getAccounts(this).getAccount(accountId);
DcMsg msg = dcContext.getMsg(msgId);
Uri resolved = new AudioSlide(this, msg).getUri();
if (resolved == null) {
throw new IOException("No file for msgId " + msgId + " in account " + accountId);
}
return dataSpec.withUri(resolved);
});
MediaSource.Factory mediaSourceFactory =
new DefaultMediaSourceFactory(this).setDataSourceFactory(dataSourceFactory);
player =
new ExoPlayer.Builder(this)
.setAudioAttributes(audioAttributes, true)
.setHandleAudioBecomingNoisy(true)
.setMediaSourceFactory(mediaSourceFactory)
.build();
// This is for click on the notification to go back to app
@@ -194,10 +194,6 @@ public class Prefs {
return true;
}
public static boolean isNewBroadcastAvailable(Context context) {
return true;
}
// ringtone
public static @NonNull Uri getNotificationRingtone(Context context) {
@@ -126,6 +126,7 @@
</LinearLayout>
<LinearLayout
android:id="@+id/webxdc_linear_layout"
android:layout_width="90dp"
android:layout_height="wrap_content"
android:gravity="center"
@@ -2,6 +2,10 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:title="@string/mark_all_as_read"
android:id="@+id/menu_mark_all_as_read" />
<item
android:title="@string/menu_mute"
android:id="@+id/menu_mute_notifications" />
+15 -2
View File
@@ -814,16 +814,23 @@
<string name="pref_background">Wallpaper</string>
<string name="pref_background_btn_default">Use Default Image</string>
<string name="pref_background_btn_gallery">Select From Gallery</string>
<!-- deprecated -->
<string name="pref_imap_folder_warn_disable_defaults">If you change this option, make sure, your server and your other clients are configured accordingly.\n\nOtherwise things may not work at all.</string>
<!-- No need to be literal here, you can also use "Use Multiple Devices", "Support Multiple Devices" or other fitting terms. However, it should fit to the wording or your language at https://delta.chat/help -->
<string name="pref_multidevice">Multi-Device Mode</string>
<string name="pref_multidevice_explain">Synchronize your messages with your other devices. Automatically enabled on adding a second device</string>
<string name="pref_multidevice_change_warn">Multi-Device Mode must be enabled when using the same profile on multiple devices. Only disable this setting if you have removed this profile from all your other devices.\n\nDisabling Multi-Device Mode while using the profile on multiple devices will result in missed messages and other problems.</string>
<!-- deprecated -->
<string name="pref_auto_folder_moves">Move automatically to DeltaChat Folder</string>
<!-- deprecated -->
<string name="pref_only_fetch_mvbox_title">Only Fetch from DeltaChat Folder</string>
<!-- deprecated -->
<string name="pref_show_emails">Show Classic Emails</string>
<!-- deprecated -->
<string name="pref_show_emails_no">No, chats only</string>
<!-- deprecated -->
<string name="pref_show_emails_accepted_contacts">For accepted contacts</string>
<!-- deprecated -->
<string name="pref_show_emails_all">All</string>
<string name="pref_experimental_features">Experimental Features</string>
<string name="pref_experimental_features_explain">These features may be unstable and may be changed or removed</string>
@@ -878,14 +885,17 @@
<!-- automatically delete message -->
<string name="delete_old_messages">Delete Old Messages</string>
<string name="autodel_device_title">Delete Messages from Device</string>
<!-- deprecated -->
<string name="autodel_server_title">Delete Messages from Server</string>
<!-- %1$d will be replaced by the number of messages, you can assume plural/lots here. %2$s will be replaced by a timespan option. -->
<string name="autodel_device_ask">Do you want to delete %1$d messages now and all newly fetched messages \"%2$s\" in the future?\n\n• This includes all media\n\n• Messages will be deleted whether they were seen or not\n\n• \"Saved messages\" will be skipped from local deletion</string>
<!-- %1$d will be replaced by the number of messages, you can assume plural/lots here. %2$s will be replaced by a timespan option. -->
<!-- deprecated -->
<string name="autodel_server_ask">Do you want to delete %1$d messages now and all newly fetched messages \"%2$s\" in the future?\n\n⚠️ This includes emails, media and \"Saved messages\" in all server folders\n\n⚠️ Do not use this function if you want to keep data on the server or if you use classic email clients as well</string>
<!-- shown below enabled autodel_server-option, should be a summary of autodel_server_ask and remind about the impact -->
<!-- deprecated -->
<string name="autodel_server_enabled_hint">This includes emails, media and \"Saved messages\" in all server folders. Do not use this function if you want to keep data on the server or if you use classic email clients as well</string>
<!-- deprecated -->
<string name="autodel_server_warn_multi_device_title">Turn on at-once deletion</string>
<!-- deprecated -->
<string name="autodel_server_warn_multi_device">If you enable at-once deletion you cannot use multiple devices on this profile.</string>
<string name="autodel_confirm">I understand, delete all these messages</string>
<!-- "At once" in the meaning of "Immediately", without any intervening time. -->
@@ -1217,6 +1227,9 @@
<string name="perm_enable_bg_reminder_title">Tap here to receive messages while Delta Chat is in the background.</string>
<string name="perm_enable_bg_already_done">You already allowed Delta Chat to receive messages in the background.\n\nIf messages still do not arrive in background, please also check your system settings.</string>
<!-- Accessibility -->
<string name="accessibility_link_action">Open %1$s</string>
<!-- device messages for updates -->
<string name="update_2_0">What\'s new?\n\n💯 End-to-end encryption is reliable and forever now. Padlocks 🔒 are gone!\n\n✉️ Classic email without end-to-end encryption is marked with a letter symbol\n\n😻 New enhanced profile screen for all your contacts\n\n🔲 New button for quick access to apps used in a chat\n\n❤️ Please donate to help us remain independent and continue to bring improvements: %1$s</string>
</resources>