mirror of
https://github.com/ArcaneChat/android.git
synced 2026-07-03 14:05:24 +02:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| db98b41aa7 | |||
| 877545518b | |||
| 0cff1066e1 | |||
| 88f528e8b8 | |||
| ee3dcd38a6 | |||
| 1af9e87af0 | |||
| dcaa19b29d | |||
| 11845da65f | |||
| 82262cb166 | |||
| e85ae5247a | |||
| 40cdeee47a | |||
| 5040fb4634 | |||
| 4313e8824b | |||
| 102c3fd82a | |||
| dc31eee4bc | |||
| 875f0e5c86 | |||
| 2b36e2dc26 | |||
| 6a9c3ab8ca | |||
| 2ab544b18e | |||
| 2630e22421 | |||
| 1b7e32ab44 | |||
| 8436909977 | |||
| 9dd44c24b3 | |||
| 5d0e85b30d | |||
| 5193dfa412 | |||
| 96490e23f8 | |||
| 7959b07d45 | |||
| c4ae7927ad | |||
| ba0515ebc5 | |||
| 349bab9510 | |||
| e26f803d1f | |||
| 67d2c89bcc |
@@ -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:
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,6 @@
|
||||
rules:
|
||||
unpinned-uses:
|
||||
config:
|
||||
policies:
|
||||
actions/*: ref-pin
|
||||
dependabot/*: ref-pin
|
||||
@@ -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
|
||||
|
||||
@@ -279,6 +279,7 @@ dependencies {
|
||||
implementation "androidx.media3:media3-session:$media3_version"
|
||||
implementation "androidx.media3:media3-ui:$media3_version"
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
|
||||
implementation 'androidx.viewpager2:viewpager2:1.1.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
|
||||
implementation 'com.fasterxml.jackson.core:jackson-databind:2.11.1' // used as JSON library
|
||||
|
||||
@@ -4,7 +4,6 @@ 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;
|
||||
@@ -12,21 +11,23 @@ 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.fragment.app.FragmentActivity;
|
||||
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 androidx.viewpager2.adapter.FragmentStateAdapter;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
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.android.material.tabs.TabLayoutMediator;
|
||||
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;
|
||||
@@ -63,7 +64,7 @@ public class AllMediaActivity extends PassphraseRequiredActionBarActivity
|
||||
private final ArrayList<TabData> tabs = new ArrayList<>();
|
||||
private Toolbar toolbar;
|
||||
private TabLayout tabLayout;
|
||||
private ViewPager viewPager;
|
||||
private ViewPager2 viewPager;
|
||||
|
||||
private @Nullable MediaController mediaController;
|
||||
private ListenableFuture<MediaController> mediaControllerFuture;
|
||||
@@ -97,8 +98,20 @@ public class AllMediaActivity extends PassphraseRequiredActionBarActivity
|
||||
isGlobalGallery() ? R.string.menu_all_media : R.string.apps_and_media);
|
||||
}
|
||||
|
||||
this.tabLayout.setupWithViewPager(viewPager);
|
||||
this.viewPager.setAdapter(new AllMediaPagerAdapter(getSupportFragmentManager()));
|
||||
AllMediaPagerAdapter adapter = new AllMediaPagerAdapter(this);
|
||||
this.viewPager.setAdapter(adapter);
|
||||
this.viewPager.registerOnPageChangeCallback(
|
||||
new ViewPager2.OnPageChangeCallback() {
|
||||
@Override
|
||||
public void onPageSelected(int position) {
|
||||
adapter.onPageChanged(position);
|
||||
}
|
||||
});
|
||||
new TabLayoutMediator(
|
||||
this.tabLayout,
|
||||
this.viewPager,
|
||||
(tab, position) -> tab.setText(getString(tabs.get(position).title)))
|
||||
.attach();
|
||||
if (getIntent().getBooleanExtra(FORCE_GALLERY, false)) {
|
||||
this.viewPager.setCurrentItem(1, false);
|
||||
}
|
||||
@@ -107,7 +120,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 +134,7 @@ public class AllMediaActivity extends PassphraseRequiredActionBarActivity
|
||||
mediaController = null;
|
||||
playbackViewModel.setMediaController(null);
|
||||
}
|
||||
playbackViewModel.setQueueProvider(null);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@@ -182,31 +198,16 @@ public class AllMediaActivity extends PassphraseRequiredActionBarActivity
|
||||
return contactId == 0 && chatId == 0;
|
||||
}
|
||||
|
||||
private class AllMediaPagerAdapter extends FragmentStatePagerAdapter {
|
||||
private Object currentFragment = null;
|
||||
private class AllMediaPagerAdapter extends FragmentStateAdapter {
|
||||
private int currentPosition = -1;
|
||||
|
||||
AllMediaPagerAdapter(FragmentManager fragmentManager) {
|
||||
super(fragmentManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
|
||||
super.setPrimaryItem(container, position, object);
|
||||
if (currentFragment != null && currentFragment != object) {
|
||||
ActionMode action = null;
|
||||
if (currentFragment instanceof MessageSelectorFragment) {
|
||||
action = ((MessageSelectorFragment) currentFragment).getActionMode();
|
||||
}
|
||||
if (action != null) {
|
||||
action.finish();
|
||||
}
|
||||
}
|
||||
currentFragment = object;
|
||||
AllMediaPagerAdapter(FragmentActivity activity) {
|
||||
super(activity);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
public Fragment createFragment(int position) {
|
||||
TabData data = tabs.get(position);
|
||||
Fragment fragment;
|
||||
Bundle args = new Bundle();
|
||||
@@ -229,13 +230,24 @@ public class AllMediaActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
public int getItemCount() {
|
||||
return tabs.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getPageTitle(int position) {
|
||||
return getString(tabs.get(position).title);
|
||||
void onPageChanged(int newPosition) {
|
||||
if (currentPosition != -1 && currentPosition != newPosition) {
|
||||
for (Fragment fragment : getSupportFragmentManager().getFragments()) {
|
||||
if (!(fragment instanceof MessageSelectorFragment)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ActionMode action = ((MessageSelectorFragment) fragment).getActionMode();
|
||||
if (action != null) {
|
||||
action.finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
currentPosition = newPosition;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -122,6 +122,9 @@ public abstract class BaseConversationListFragment extends Fragment implements A
|
||||
|
||||
Context context = getContext();
|
||||
if (context != null) {
|
||||
if (SendRelayedMessageUtil.containsVideoType(context, uris)) {
|
||||
message += "\n\n" + getString(R.string.videos_sent_without_recoding);
|
||||
}
|
||||
new AlertDialog.Builder(context)
|
||||
.setMessage(message)
|
||||
.setCancelable(false)
|
||||
@@ -129,16 +132,11 @@ public abstract class BaseConversationListFragment extends Fragment implements A
|
||||
.setPositiveButton(
|
||||
R.string.menu_send,
|
||||
(dialog, which) -> {
|
||||
Activity activity = getActivity();
|
||||
if (activity == null) {
|
||||
return;
|
||||
}
|
||||
SendRelayedMessageUtil.immediatelyRelay(
|
||||
activity,
|
||||
selectedChats.toArray(new Long[selectedChats.size()]),
|
||||
() -> activity.finish());
|
||||
getActivity(), selectedChats.toArray(new Long[selectedChats.size()]));
|
||||
actionMode.finish();
|
||||
actionMode = null;
|
||||
getActivity().finish();
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
@@ -860,6 +870,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
private void askSendingFiles(ArrayList<Uri> uriList, Runnable onConfirm) {
|
||||
String message =
|
||||
String.format(getString(R.string.ask_send_files_to_chat), uriList.size(), dcChat.getName());
|
||||
if (SendRelayedMessageUtil.containsVideoType(context, uriList)) {
|
||||
message += "\n\n" + getString(R.string.videos_sent_without_recoding);
|
||||
}
|
||||
new AlertDialog.Builder(this)
|
||||
.setMessage(message)
|
||||
.setCancelable(false)
|
||||
@@ -1116,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);
|
||||
|
||||
@@ -1264,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;
|
||||
@@ -1386,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();
|
||||
|
||||
@@ -626,18 +626,14 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
|
||||
final DcContext dcContext = DcHelper.getContext(this);
|
||||
int fwdAccId = getForwardedMessageAccountId(this);
|
||||
if (fwdAccId == dcContext.getAccountId() && dcContext.getChat(chatId).isSelfTalk()) {
|
||||
SendRelayedMessageUtil.immediatelyRelay(
|
||||
this,
|
||||
chatId,
|
||||
() -> {
|
||||
Toast.makeText(
|
||||
this,
|
||||
DynamicTheme.getCheckmarkEmoji(this) + " " + getString(R.string.saved),
|
||||
Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
handleResetRelaying();
|
||||
finish();
|
||||
});
|
||||
SendRelayedMessageUtil.immediatelyRelay(this, chatId);
|
||||
Toast.makeText(
|
||||
this,
|
||||
DynamicTheme.getCheckmarkEmoji(this) + " " + getString(R.string.saved),
|
||||
Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
handleResetRelaying();
|
||||
finish();
|
||||
} else {
|
||||
Intent intent = new Intent(this, ConversationActivity.class);
|
||||
intent.putExtra(ConversationActivity.ACCOUNT_ID_EXTRA, dcContext.getAccountId());
|
||||
|
||||
@@ -33,14 +33,13 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.FrameLayout;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.viewpager.widget.PagerAdapter;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
import com.b44t.messenger.DcChat;
|
||||
import com.b44t.messenger.DcContext;
|
||||
import com.b44t.messenger.DcMediaGalleryElement;
|
||||
@@ -88,7 +87,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
@Nullable private DcMsg messageRecord;
|
||||
private DcContext dcContext;
|
||||
private MediaItem initialMedia;
|
||||
private ViewPager mediaPager;
|
||||
private ViewPager2 mediaPager;
|
||||
private Recipient conversationRecipient;
|
||||
private boolean leftIsRecent;
|
||||
|
||||
@@ -190,7 +189,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
private void initializeViews() {
|
||||
mediaPager = findViewById(R.id.media_pager);
|
||||
mediaPager.setOffscreenPageLimit(1);
|
||||
mediaPager.addOnPageChangeListener(new ViewPagerListener());
|
||||
mediaPager.registerOnPageChangeCallback(new ViewPagerListener());
|
||||
}
|
||||
|
||||
private void initializeResources() {
|
||||
@@ -261,7 +260,6 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
private int cleanupMedia() {
|
||||
int restartItem = mediaPager.getCurrentItem();
|
||||
|
||||
mediaPager.removeAllViews();
|
||||
mediaPager.setAdapter(null);
|
||||
|
||||
return restartItem;
|
||||
@@ -483,11 +481,11 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
DcMediaPagerAdapter adapter =
|
||||
new DcMediaPagerAdapter(this, GlideApp.with(this), getWindow(), data, leftIsRecent);
|
||||
mediaPager.setAdapter(adapter);
|
||||
adapter.setActive(true);
|
||||
mediaPager.setAdapter(adapter);
|
||||
|
||||
if (restartItem < 0) mediaPager.setCurrentItem(data.getPosition());
|
||||
else mediaPager.setCurrentItem(restartItem);
|
||||
if (restartItem < 0) mediaPager.setCurrentItem(data.getPosition(), false);
|
||||
else mediaPager.setCurrentItem(restartItem, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -526,7 +524,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
}
|
||||
|
||||
private static class SingleItemPagerAdapter extends PagerAdapter implements MediaItemAdapter {
|
||||
private static class SingleItemPagerAdapter extends RecyclerView.Adapter<SingleItemPagerAdapter.MediaViewHolder> implements MediaItemAdapter {
|
||||
|
||||
private final GlideRequests glideRequests;
|
||||
private final Window window;
|
||||
@@ -555,37 +553,29 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
public int getItemCount() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
|
||||
return view == object;
|
||||
public MediaViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
return new MediaViewHolder(inflater.inflate(R.layout.media_view_page, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Object instantiateItem(@NonNull ViewGroup container, int position) {
|
||||
View itemView = inflater.inflate(R.layout.media_view_page, container, false);
|
||||
MediaView mediaView = itemView.findViewById(R.id.media_view);
|
||||
|
||||
public void onBindViewHolder(@NonNull MediaViewHolder holder, int position) {
|
||||
try {
|
||||
mediaView.set(glideRequests, window, uri, name, mediaType, size, true);
|
||||
holder.mediaView.set(glideRequests, window, uri, name, mediaType, size, true);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
||||
container.addView(itemView);
|
||||
|
||||
return itemView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
|
||||
MediaView mediaView = ((FrameLayout) object).findViewById(R.id.media_view);
|
||||
mediaView.cleanup();
|
||||
|
||||
container.removeView((FrameLayout) object);
|
||||
public void onViewRecycled(@NonNull MediaViewHolder holder) {
|
||||
super.onViewRecycled(holder);
|
||||
holder.mediaView.cleanup();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -595,9 +585,18 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
@Override
|
||||
public void pause(int position) {}
|
||||
|
||||
static class MediaViewHolder extends RecyclerView.ViewHolder {
|
||||
final MediaView mediaView;
|
||||
|
||||
MediaViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
mediaView = itemView.findViewById(R.id.media_view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class DcMediaPagerAdapter extends PagerAdapter implements MediaItemAdapter {
|
||||
private static class DcMediaPagerAdapter extends RecyclerView.Adapter<DcMediaPagerAdapter.MediaViewHolder> implements MediaItemAdapter {
|
||||
|
||||
private final WeakHashMap<Integer, MediaView> mediaViews = new WeakHashMap<>();
|
||||
|
||||
@@ -630,21 +629,21 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
public int getItemCount() {
|
||||
if (!active) return 0;
|
||||
else return gallery.getCount();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
|
||||
return view == object;
|
||||
public MediaViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View itemView =
|
||||
LayoutInflater.from(context).inflate(R.layout.media_view_page, parent, false);
|
||||
return new MediaViewHolder(itemView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Object instantiateItem(@NonNull ViewGroup container, int position) {
|
||||
View itemView =
|
||||
LayoutInflater.from(context).inflate(R.layout.media_view_page, container, false);
|
||||
MediaView mediaView = itemView.findViewById(R.id.media_view);
|
||||
public void onBindViewHolder(@NonNull MediaViewHolder holder, int position) {
|
||||
boolean autoplay = position == autoPlayPosition;
|
||||
int cursorPosition = getCursorPosition(position);
|
||||
|
||||
@@ -656,7 +655,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
try {
|
||||
//noinspection ConstantConditions
|
||||
mediaView.set(
|
||||
holder.mediaView.set(
|
||||
glideRequests,
|
||||
window,
|
||||
Uri.fromFile(msg.getFileAsFile()),
|
||||
@@ -668,19 +667,17 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
||||
mediaViews.put(position, mediaView);
|
||||
container.addView(itemView);
|
||||
|
||||
return itemView;
|
||||
mediaViews.put(position, holder.mediaView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
|
||||
MediaView mediaView = ((FrameLayout) object).findViewById(R.id.media_view);
|
||||
mediaView.cleanup();
|
||||
|
||||
mediaViews.remove(position);
|
||||
container.removeView((FrameLayout) object);
|
||||
public void onViewRecycled(@NonNull MediaViewHolder holder) {
|
||||
super.onViewRecycled(holder);
|
||||
int pos = holder.getBindingAdapterPosition();
|
||||
if (pos != RecyclerView.NO_POSITION) {
|
||||
mediaViews.remove(pos);
|
||||
}
|
||||
holder.mediaView.cleanup();
|
||||
}
|
||||
|
||||
public MediaItem getMediaItemFor(int position) {
|
||||
@@ -710,6 +707,15 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity
|
||||
if (leftIsRecent) return position;
|
||||
else return gallery.getCount() - 1 - position;
|
||||
}
|
||||
|
||||
static class MediaViewHolder extends RecyclerView.ViewHolder {
|
||||
final MediaView mediaView;
|
||||
|
||||
MediaViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
mediaView = itemView.findViewById(R.id.media_view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class MediaItem {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import org.thoughtcrime.securesms.components.viewpager.HackyViewPager;
|
||||
|
||||
/** An implementation of {@link ViewPager} that disables swiping when the view is disabled. */
|
||||
public class ControllableViewPager extends HackyViewPager {
|
||||
|
||||
public ControllableViewPager(@NonNull Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public ControllableViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent ev) {
|
||||
return isEnabled() && super.onTouchEvent(ev);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(MotionEvent ev) {
|
||||
return isEnabled() && super.onInterceptTouchEvent(ev);
|
||||
}
|
||||
}
|
||||
+109
-36
@@ -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);
|
||||
|
||||
+41
@@ -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;
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
package org.thoughtcrime.securesms.components.viewpager;
|
||||
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
public abstract class ExtendedOnPageChangedListener implements ViewPager.OnPageChangeListener {
|
||||
public abstract class ExtendedOnPageChangedListener extends ViewPager2.OnPageChangeCallback {
|
||||
|
||||
private Integer currentPage = null;
|
||||
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.viewpager;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.MotionEvent;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
/**
|
||||
* Hacky fix for http://code.google.com/p/android/issues/detail?id=18990
|
||||
*
|
||||
* <p>ScaleGestureDetector seems to mess up the touch events, which means that ViewGroups which make
|
||||
* use of onInterceptTouchEvent throw a lot of IllegalArgumentException: pointerIndex out of range.
|
||||
*
|
||||
* <p>There's not much I can do in my code for now, but we can mask the result by just catching the
|
||||
* problem and ignoring it.
|
||||
*
|
||||
* @author Chris Banes
|
||||
*/
|
||||
public class HackyViewPager extends ViewPager {
|
||||
|
||||
private static final String TAG = "HackyViewPager";
|
||||
|
||||
public HackyViewPager(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public HackyViewPager(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(MotionEvent ev) {
|
||||
try {
|
||||
return super.onInterceptTouchEvent(ev);
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.w(TAG, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()) {
|
||||
|
||||
@@ -15,10 +15,11 @@ import android.view.View;
|
||||
import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentStatePagerAdapter;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.google.android.material.tabs.TabLayoutMediator;
|
||||
import com.google.zxing.BinaryBitmap;
|
||||
import com.google.zxing.MultiFormatReader;
|
||||
import com.google.zxing.NotFoundException;
|
||||
@@ -48,8 +49,9 @@ public class QrActivity extends BaseActionBarActivity implements View.OnClickLis
|
||||
private static final int TAB_SCAN = 1;
|
||||
|
||||
private TabLayout tabLayout;
|
||||
private ViewPager viewPager;
|
||||
private ViewPager2 viewPager;
|
||||
private QrShowFragment qrShowFragment;
|
||||
private ProfilePagerAdapter adapter;
|
||||
private boolean scanRelay;
|
||||
|
||||
@Override
|
||||
@@ -68,7 +70,7 @@ public class QrActivity extends BaseActionBarActivity implements View.OnClickLis
|
||||
qrShowFragment = new QrShowFragment(this);
|
||||
tabLayout = ViewUtil.findById(this, R.id.tab_layout);
|
||||
viewPager = ViewUtil.findById(this, R.id.pager);
|
||||
ProfilePagerAdapter adapter = new ProfilePagerAdapter(this, getSupportFragmentManager());
|
||||
adapter = new ProfilePagerAdapter(this);
|
||||
viewPager.setAdapter(adapter);
|
||||
|
||||
setSupportActionBar(ViewUtil.findById(this, R.id.toolbar));
|
||||
@@ -76,38 +78,36 @@ public class QrActivity extends BaseActionBarActivity implements View.OnClickLis
|
||||
getSupportActionBar().setTitle(scanRelay ? R.string.add_transport : R.string.menu_new_contact);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
viewPager.setCurrentItem(scanRelay ? TAB_SCAN : TAB_SHOW);
|
||||
viewPager.setCurrentItem(scanRelay ? TAB_SCAN : TAB_SHOW, false);
|
||||
if (scanRelay) tabLayout.setVisibility(View.GONE);
|
||||
|
||||
viewPager.addOnPageChangeListener(
|
||||
new ViewPager.OnPageChangeListener() {
|
||||
@Override
|
||||
public void onPageScrolled(
|
||||
int position, float positionOffset, int positionOffsetPixels) {}
|
||||
|
||||
viewPager.registerOnPageChangeCallback(
|
||||
new ViewPager2.OnPageChangeCallback() {
|
||||
@Override
|
||||
public void onPageSelected(int position) {
|
||||
QrActivity.this.invalidateOptionsMenu();
|
||||
checkPermissions(position, adapter, viewPager);
|
||||
checkPermissions(position, viewPager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageScrollStateChanged(int state) {}
|
||||
});
|
||||
|
||||
tabLayout.setupWithViewPager(viewPager);
|
||||
new TabLayoutMediator(
|
||||
tabLayout,
|
||||
viewPager,
|
||||
(tab, position) -> tab.setText(adapter.getPageTitle(position)))
|
||||
.attach();
|
||||
}
|
||||
|
||||
private void checkPermissions(int position, ProfilePagerAdapter adapter, ViewPager viewPager) {
|
||||
private void checkPermissions(int position, ViewPager2 viewPager) {
|
||||
if (position == TAB_SCAN) {
|
||||
Permissions.with(QrActivity.this)
|
||||
.request(Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(getString(R.string.perm_explain_access_to_camera_denied))
|
||||
.onAllGranted(
|
||||
() ->
|
||||
((QrScanFragment) adapter.getItem(TAB_SCAN))
|
||||
.handleQrScanWithPermissions(QrActivity.this))
|
||||
() -> {
|
||||
QrScanFragment qrScanFrag = adapter.getQrScanFragment();
|
||||
qrScanFrag.handleQrScanWithPermissions(QrActivity.this);
|
||||
})
|
||||
.onAnyDenied(
|
||||
() -> {
|
||||
if (scanRelay) {
|
||||
@@ -225,47 +225,41 @@ public class QrActivity extends BaseActionBarActivity implements View.OnClickLis
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
viewPager.setCurrentItem(TAB_SCAN);
|
||||
viewPager.setCurrentItem(TAB_SCAN, true);
|
||||
}
|
||||
|
||||
private class ProfilePagerAdapter extends FragmentStatePagerAdapter {
|
||||
private class ProfilePagerAdapter extends FragmentStateAdapter {
|
||||
|
||||
private final QrActivity activity;
|
||||
private final QrScanFragment qrScanFragment = new QrScanFragment();
|
||||
|
||||
ProfilePagerAdapter(QrActivity activity, FragmentManager fragmentManager) {
|
||||
super(fragmentManager, FragmentStatePagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
|
||||
this.activity = activity;
|
||||
ProfilePagerAdapter(FragmentActivity activity) {
|
||||
super(activity);
|
||||
}
|
||||
|
||||
QrScanFragment getQrScanFragment() {
|
||||
return qrScanFragment;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
Fragment fragment;
|
||||
|
||||
public Fragment createFragment(int position) {
|
||||
switch (position) {
|
||||
case TAB_SHOW:
|
||||
fragment = activity.qrShowFragment;
|
||||
break;
|
||||
|
||||
return qrShowFragment;
|
||||
default:
|
||||
fragment = new QrScanFragment();
|
||||
break;
|
||||
return qrScanFragment;
|
||||
}
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
public int getItemCount() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getPageTitle(int position) {
|
||||
CharSequence getPageTitle(int position) {
|
||||
switch (position) {
|
||||
case TAB_SHOW:
|
||||
return getString(R.string.qrshow_title);
|
||||
|
||||
default:
|
||||
return getString(R.string.qrscan_title);
|
||||
}
|
||||
|
||||
@@ -20,10 +20,10 @@ import android.view.MenuItem;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentStatePagerAdapter;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.google.android.material.tabs.TabLayoutMediator;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
public class StickerSelectActivity extends FragmentActivity
|
||||
@@ -47,15 +47,16 @@ public class StickerSelectActivity extends FragmentActivity
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.scribble_select_sticker_activity);
|
||||
|
||||
ViewPager viewPager = findViewById(R.id.camera_sticker_pager);
|
||||
viewPager.setAdapter(new StickerPagerAdapter(getSupportFragmentManager(), this));
|
||||
ViewPager2 viewPager = findViewById(R.id.camera_sticker_pager);
|
||||
StickerPagerAdapter adapter = new StickerPagerAdapter(this, this);
|
||||
viewPager.setAdapter(adapter);
|
||||
|
||||
TabLayout tabLayout = findViewById(R.id.camera_sticker_tabs);
|
||||
tabLayout.setupWithViewPager(viewPager);
|
||||
|
||||
for (int i = 0; i < tabLayout.getTabCount(); i++) {
|
||||
tabLayout.getTabAt(i).setIcon(TAB_TITLES[i]);
|
||||
}
|
||||
new TabLayoutMediator(
|
||||
tabLayout,
|
||||
viewPager,
|
||||
(tab, position) -> tab.setIcon(TAB_TITLES[position]))
|
||||
.attach();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -75,13 +76,13 @@ public class StickerSelectActivity extends FragmentActivity
|
||||
finish();
|
||||
}
|
||||
|
||||
static class StickerPagerAdapter extends FragmentStatePagerAdapter {
|
||||
static class StickerPagerAdapter extends FragmentStateAdapter {
|
||||
|
||||
private final Fragment[] fragments;
|
||||
|
||||
StickerPagerAdapter(
|
||||
FragmentManager fm, StickerSelectFragment.StickerSelectionListener listener) {
|
||||
super(fm);
|
||||
FragmentActivity activity, StickerSelectFragment.StickerSelectionListener listener) {
|
||||
super(activity);
|
||||
|
||||
this.fragments =
|
||||
new Fragment[] {
|
||||
@@ -98,12 +99,12 @@ public class StickerSelectActivity extends FragmentActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
public Fragment createFragment(int position) {
|
||||
return fragments[position];
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
public int getItemCount() {
|
||||
return fragments.length;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -9,7 +9,6 @@ import static org.thoughtcrime.securesms.util.ShareUtil.isSharing;
|
||||
import static org.thoughtcrime.securesms.util.ShareUtil.resetRelayingMessageContent;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
@@ -27,75 +26,57 @@ import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.thoughtcrime.securesms.ConversationListRelayingActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
|
||||
import org.thoughtcrime.securesms.video.recode.VideoRecoder;
|
||||
|
||||
public class SendRelayedMessageUtil {
|
||||
private static final String TAG = SendRelayedMessageUtil.class.getSimpleName();
|
||||
|
||||
public static void immediatelyRelay(Activity activity, int chatId) {
|
||||
immediatelyRelay(activity, chatId, null);
|
||||
}
|
||||
|
||||
public static void immediatelyRelay(Activity activity, int chatId, Runnable onCompletion) {
|
||||
immediatelyRelay(activity, new Long[] {(long) chatId}, onCompletion);
|
||||
immediatelyRelay(activity, new Long[] {(long) chatId});
|
||||
}
|
||||
|
||||
public static void immediatelyRelay(Activity activity, final Long[] chatIds) {
|
||||
immediatelyRelay(activity, chatIds, null);
|
||||
}
|
||||
|
||||
public static void immediatelyRelay(
|
||||
Activity activity, final Long[] chatIds, Runnable onCompletion) {
|
||||
ConversationListRelayingActivity.finishActivity();
|
||||
if (isForwarding(activity)) {
|
||||
int forwardedMsgAccId = getForwardedMessageAccountId(activity);
|
||||
int[] forwardedMessageIDs = getForwardedMessageIDs(activity);
|
||||
resetRelayingMessageContent(activity);
|
||||
if (forwardedMessageIDs == null || forwardedMsgAccId <= 0) {
|
||||
runOnCompletion(onCompletion);
|
||||
return;
|
||||
}
|
||||
if (forwardedMessageIDs == null || forwardedMsgAccId <= 0) return;
|
||||
|
||||
Util.runOnAnyBackgroundThread(
|
||||
() -> {
|
||||
try {
|
||||
DcContext dcContext = DcHelper.getContext(activity);
|
||||
int accId = dcContext.getAccountId();
|
||||
if (forwardedMsgAccId != accId) {
|
||||
Rpc rpc = DcHelper.getRpc(activity);
|
||||
List<Integer> list = Util.toList(forwardedMessageIDs);
|
||||
for (long longChatId : chatIds) {
|
||||
try {
|
||||
rpc.forwardMessagesToAccount(forwardedMsgAccId, list, accId, (int) longChatId);
|
||||
} catch (RpcException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
DcContext dcContext = DcHelper.getContext(activity);
|
||||
int accId = dcContext.getAccountId();
|
||||
if (forwardedMsgAccId != accId) {
|
||||
Rpc rpc = DcHelper.getRpc(activity);
|
||||
List<Integer> list = Util.toList(forwardedMessageIDs);
|
||||
for (long longChatId : chatIds) {
|
||||
int chatId = (int) longChatId;
|
||||
if (dcContext.getChat(chatId).isSelfTalk()) {
|
||||
for (int msgId : forwardedMessageIDs) {
|
||||
DcMsg msg = dcContext.getMsg(msgId);
|
||||
if (msg.canSave() && msg.getSavedMsgId() == 0 && msg.getChatId() != chatId) {
|
||||
dcContext.saveMsgs(new int[] {msgId});
|
||||
} else {
|
||||
handleForwarding(activity, chatId, new int[] {msgId});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
handleForwarding(activity, chatId, forwardedMessageIDs);
|
||||
try {
|
||||
rpc.forwardMessagesToAccount(forwardedMsgAccId, list, accId, (int) longChatId);
|
||||
} catch (RpcException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
runOnCompletion(onCompletion);
|
||||
return;
|
||||
}
|
||||
|
||||
for (long longChatId : chatIds) {
|
||||
int chatId = (int) longChatId;
|
||||
if (dcContext.getChat(chatId).isSelfTalk()) {
|
||||
for (int msgId : forwardedMessageIDs) {
|
||||
DcMsg msg = dcContext.getMsg(msgId);
|
||||
if (msg.canSave() && msg.getSavedMsgId() == 0 && msg.getChatId() != chatId) {
|
||||
dcContext.saveMsgs(new int[] {msgId});
|
||||
} else {
|
||||
handleForwarding(activity, chatId, new int[] {msgId});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
handleForwarding(activity, chatId, forwardedMessageIDs);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (isSharing(activity)) {
|
||||
@@ -107,30 +88,14 @@ public class SendRelayedMessageUtil {
|
||||
resetRelayingMessageContent(activity);
|
||||
Util.runOnAnyBackgroundThread(
|
||||
() -> {
|
||||
try {
|
||||
for (long chatId : chatIds) {
|
||||
sendMultipleMsgs(
|
||||
activity, (int) chatId, sharedUris, msgType, sharedHtml, subject, sharedText);
|
||||
}
|
||||
} finally {
|
||||
runOnCompletion(onCompletion);
|
||||
for (long chatId : chatIds) {
|
||||
sendMultipleMsgs(
|
||||
activity, (int) chatId, sharedUris, msgType, sharedHtml, subject, sharedText);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
runOnCompletion(onCompletion);
|
||||
}
|
||||
}
|
||||
|
||||
private static void runOnCompletion(Runnable onCompletion) {
|
||||
Util.runOnMain(
|
||||
() -> {
|
||||
ConversationListRelayingActivity.finishActivity();
|
||||
if (onCompletion != null) {
|
||||
onCompletion.run();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void handleForwarding(Context context, int chatId, int[] forwardedMessageIDs) {
|
||||
DcContext dcContext = DcHelper.getContext(context);
|
||||
dcContext.forwardMsgs(forwardedMessageIDs, chatId);
|
||||
@@ -153,64 +118,17 @@ public class SendRelayedMessageUtil {
|
||||
ArrayList<Uri> uris = sharedUris;
|
||||
String text = sharedText;
|
||||
|
||||
boolean hasVideos = containsVideoType(context, uris);
|
||||
AtomicReference<ProgressDialog> progressDialogRef = new AtomicReference<>(null);
|
||||
if (hasVideos && context instanceof Activity) {
|
||||
Activity activity = (Activity) context;
|
||||
Util.runOnMain(
|
||||
() -> {
|
||||
try {
|
||||
if (!activity.isFinishing()) {
|
||||
progressDialogRef.set(
|
||||
ProgressDialog.show(
|
||||
activity, "", context.getString(R.string.one_moment), true, false));
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
Log.w(TAG, "Failed to show recoding progress dialog", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (uris.size() == 1) {
|
||||
DcMsg msg = createMessage(context, uris.get(0), msgType, sharedHtml, subject, text);
|
||||
if (msg.getType() == DcMsg.DC_MSG_VIDEO) {
|
||||
if (!VideoRecoder.prepareVideo(context, chatId, msg)) {
|
||||
dismissProgressDialog(progressDialogRef);
|
||||
return;
|
||||
}
|
||||
}
|
||||
dcContext.sendMsg(chatId, msg);
|
||||
dcContext.sendMsg(
|
||||
chatId, createMessage(context, uris.get(0), msgType, sharedHtml, subject, text));
|
||||
} else {
|
||||
if (text != null || sharedHtml != null) {
|
||||
dcContext.sendMsg(chatId, createMessage(context, null, null, sharedHtml, subject, text));
|
||||
}
|
||||
for (Uri uri : uris) {
|
||||
DcMsg msg = createMessage(context, uri, null, null, subject, null);
|
||||
if (msg.getType() == DcMsg.DC_MSG_VIDEO) {
|
||||
if (!VideoRecoder.prepareVideo(context, chatId, msg)) {
|
||||
Log.w(TAG, "prepareVideo failed for " + uri + ", skipping");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
dcContext.sendMsg(chatId, msg);
|
||||
dcContext.sendMsg(chatId, createMessage(context, uri, null, null, subject, null));
|
||||
}
|
||||
}
|
||||
|
||||
dismissProgressDialog(progressDialogRef);
|
||||
}
|
||||
|
||||
private static void dismissProgressDialog(AtomicReference<ProgressDialog> progressDialogRef) {
|
||||
ProgressDialog dialog = progressDialogRef.get();
|
||||
if (dialog != null) {
|
||||
Util.runOnMain(
|
||||
() -> {
|
||||
try {
|
||||
dialog.dismiss();
|
||||
} catch (final IllegalArgumentException e) {
|
||||
// The activity is finishing/destroyed, do nothing.
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean containsVideoType(Context context, ArrayList<Uri> uris) {
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.viewpager.widget.ViewPager
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/pager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.viewpager.widget.ViewPager
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/pager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
||||
@@ -126,6 +126,7 @@
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/webxdc_linear_layout"
|
||||
android:layout_width="90dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
android:fitsSystemWindows="true"
|
||||
android:background="@color/gray95">
|
||||
|
||||
<org.thoughtcrime.securesms.components.viewpager.HackyViewPager
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/media_pager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
app:tabSelectedTextColor="@color/white"
|
||||
app:tabMode="fixed" />
|
||||
|
||||
<androidx.viewpager.widget.ViewPager
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/camera_sticker_pager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0px"
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user