diff --git a/.github/workflows/artifacts.yml b/.github/workflows/artifacts.yml index 4267113ba..f0b819ed8 100644 --- a/.github/workflows/artifacts.yml +++ b/.github/workflows/artifacts.yml @@ -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: diff --git a/.github/workflows/cache-core.yml b/.github/workflows/cache-core.yml index 2e00c8d50..8db29fcf9 100644 --- a/.github/workflows/cache-core.yml +++ b/.github/workflows/cache-core.yml @@ -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 diff --git a/.github/workflows/code-format.yml b/.github/workflows/code-format.yml index ab4c42521..2eb46e769 100644 --- a/.github/workflows/code-format.yml +++ b/.github/workflows/code-format.yml @@ -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 diff --git a/.github/workflows/preview-apk.yml b/.github/workflows/preview-apk.yml index f893cf25b..604e2bdee 100644 --- a/.github/workflows/preview-apk.yml +++ b/.github/workflows/preview-apk.yml @@ -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 diff --git a/.github/workflows/zizmor-scan.yml b/.github/workflows/zizmor-scan.yml new file mode 100644 index 000000000..af97e663b --- /dev/null +++ b/.github/workflows/zizmor-scan.yml @@ -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 diff --git a/.github/zizmor.yml b/.github/zizmor.yml new file mode 100644 index 000000000..a40e4188f --- /dev/null +++ b/.github/zizmor.yml @@ -0,0 +1,6 @@ +rules: + unpinned-uses: + config: + policies: + actions/*: ref-pin + dependabot/*: ref-pin diff --git a/CHANGELOG-upstream.md b/CHANGELOG-upstream.md index b10b747ea..f9627a1f1 100644 --- a/CHANGELOG-upstream.md +++ b/CHANGELOG-upstream.md @@ -4,11 +4,15 @@ * 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) * 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 map if user clicks "Location streaming enabled" system message ## v2.49.0 2026-04 diff --git a/src/main/java/org/thoughtcrime/securesms/AllMediaActivity.java b/src/main/java/org/thoughtcrime/securesms/AllMediaActivity.java index 6482ca308..d7a824f27 100644 --- a/src/main/java/org/thoughtcrime/securesms/AllMediaActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/AllMediaActivity.java @@ -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(); } diff --git a/src/main/java/org/thoughtcrime/securesms/ConversationActivity.java b/src/main/java/org/thoughtcrime/securesms/ConversationActivity.java index dfcfa8ea8..038512368 100644 --- a/src/main/java/org/thoughtcrime/securesms/ConversationActivity.java +++ b/src/main/java/org/thoughtcrime/securesms/ConversationActivity.java @@ -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(); } diff --git a/src/main/java/org/thoughtcrime/securesms/ConversationFragment.java b/src/main/java/org/thoughtcrime/securesms/ConversationFragment.java index c111ed63f..bb031d26d 100644 --- a/src/main/java/org/thoughtcrime/securesms/ConversationFragment.java +++ b/src/main/java/org/thoughtcrime/securesms/ConversationFragment.java @@ -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); diff --git a/src/main/java/org/thoughtcrime/securesms/components/audioplay/AudioPlaybackViewModel.java b/src/main/java/org/thoughtcrime/securesms/components/audioplay/AudioPlaybackViewModel.java index 1c9c71a12..48229495f 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/audioplay/AudioPlaybackViewModel.java +++ b/src/main/java/org/thoughtcrime/securesms/components/audioplay/AudioPlaybackViewModel.java @@ -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 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 items, String mediaId) { + for (int i = 0; i < items.size(); i++) { + if (mediaId.equals(items.get(i).mediaId)) return i; + } + return -1; } public LiveData> 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 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() { diff --git a/src/main/java/org/thoughtcrime/securesms/components/audioplay/AudioView.java b/src/main/java/org/thoughtcrime/securesms/components/audioplay/AudioView.java index 32466c4d1..37574b708 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/audioplay/AudioView.java +++ b/src/main/java/org/thoughtcrime/securesms/components/audioplay/AudioView.java @@ -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 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); diff --git a/src/main/java/org/thoughtcrime/securesms/components/audioplay/ChatAudioQueueProvider.java b/src/main/java/org/thoughtcrime/securesms/components/audioplay/ChatAudioQueueProvider.java new file mode 100644 index 000000000..d100ad27f --- /dev/null +++ b/src/main/java/org/thoughtcrime/securesms/components/audioplay/ChatAudioQueueProvider.java @@ -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 buildAudioQueue() { + DcContext dcContext = DcHelper.getContext(context); + int[] msgIds = dcContext.getChatMedia(chatId, DcMsg.DC_MSG_AUDIO, DcMsg.DC_MSG_VOICE, 0); + + List 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; + } +} diff --git a/src/main/java/org/thoughtcrime/securesms/components/emoji/StickerPickerView.java b/src/main/java/org/thoughtcrime/securesms/components/emoji/StickerPickerView.java index 5a090021a..fa2172690 100644 --- a/src/main/java/org/thoughtcrime/securesms/components/emoji/StickerPickerView.java +++ b/src/main/java/org/thoughtcrime/securesms/components/emoji/StickerPickerView.java @@ -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; } diff --git a/src/main/java/org/thoughtcrime/securesms/connect/DcContactsLoader.java b/src/main/java/org/thoughtcrime/securesms/connect/DcContactsLoader.java index 93e72f4b1..62d6c1392 100644 --- a/src/main/java/org/thoughtcrime/securesms/connect/DcContactsLoader.java +++ b/src/main/java/org/thoughtcrime/securesms/connect/DcContactsLoader.java @@ -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 { @@ -53,10 +52,7 @@ public class DcContactsLoader extends AsyncLoader { } 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 = diff --git a/src/main/java/org/thoughtcrime/securesms/geolocation/PlatformLocationSource.java b/src/main/java/org/thoughtcrime/securesms/geolocation/PlatformLocationSource.java index d79584ab5..342cec6a9 100644 --- a/src/main/java/org/thoughtcrime/securesms/geolocation/PlatformLocationSource.java +++ b/src/main/java/org/thoughtcrime/securesms/geolocation/PlatformLocationSource.java @@ -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); diff --git a/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java b/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java index 3fbd889cd..4276beb72 100644 --- a/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java +++ b/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java @@ -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()) { diff --git a/src/main/java/org/thoughtcrime/securesms/service/AudioPlaybackService.java b/src/main/java/org/thoughtcrime/securesms/service/AudioPlaybackService.java index 5d00e96d3..a63c59b2a 100644 --- a/src/main/java/org/thoughtcrime/securesms/service/AudioPlaybackService.java +++ b/src/main/java/org/thoughtcrime/securesms/service/AudioPlaybackService.java @@ -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 diff --git a/src/main/java/org/thoughtcrime/securesms/util/Prefs.java b/src/main/java/org/thoughtcrime/securesms/util/Prefs.java index 3565fcc22..df5ed43ef 100644 --- a/src/main/java/org/thoughtcrime/securesms/util/Prefs.java +++ b/src/main/java/org/thoughtcrime/securesms/util/Prefs.java @@ -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) {