mirror of
https://github.com/ArcaneChat/android.git
synced 2026-07-03 14:05:24 +02:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 93427ba412 | |||
| 30124de2a8 | |||
| 9ffc904ae5 | |||
| 2704749b44 | |||
| 1c174b5b70 | |||
| f022316ad7 | |||
| 3386f5c5f7 | |||
| 57eead3a34 | |||
| 82118db71b | |||
| cebfa12142 | |||
| 2889266522 | |||
| 5e6fccf143 | |||
| 71ed333468 | |||
| b66bc1f863 | |||
| aebd5c66f7 | |||
| f319ba2b83 | |||
| 93f12e7367 | |||
| 10acb07f82 | |||
| 13374df709 | |||
| 1ff4e069ea | |||
| 34be7aab17 | |||
| 59139ed242 | |||
| 11f3964bdc | |||
| 7683408d18 | |||
| b74e793654 | |||
| 4bd74324d2 | |||
| 9a121b3039 | |||
| a9832c9c53 |
@@ -15,6 +15,7 @@
|
||||
* Add indication for blocked contacts in user profile
|
||||
* Allow to start calls with video disabled
|
||||
* Show hint for empty contact search results
|
||||
* Add background playing for voice messages and other audio files
|
||||
* Fix: Show dialog if pasted QR codes are invalid
|
||||
* Fix: Refresh chat list when returning from conversation if selected profile changed
|
||||
* Fix: Update menu when using "select all" in contact selection
|
||||
|
||||
+9
-1
@@ -68,6 +68,7 @@ android {
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
coreLibraryDesugaringEnabled true
|
||||
}
|
||||
packagingOptions {
|
||||
jniLibs {
|
||||
@@ -211,6 +212,10 @@ dependencies {
|
||||
implementation "io.noties.markwon:inline-parser:$markwon_version"
|
||||
implementation 'com.airbnb.android:lottie:4.2.2' // Lottie animations support.
|
||||
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
|
||||
|
||||
def media3_version = "1.8.0" // 1.9.0 need minSdkVersion 23
|
||||
|
||||
implementation 'androidx.concurrent:concurrent-futures:1.3.0'
|
||||
implementation 'androidx.sharetarget:sharetarget:1.2.0'
|
||||
implementation 'androidx.webkit:webkit:1.14.0'
|
||||
@@ -231,8 +236,11 @@ dependencies {
|
||||
implementation 'androidx.work:work-runtime:2.9.1'
|
||||
implementation 'androidx.emoji2:emoji2-emojipicker:1.5.0'
|
||||
implementation 'com.google.guava:guava:31.1-android'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-core:2.19.1' // plays video and audio
|
||||
implementation 'com.google.android.exoplayer:exoplayer-core:2.19.1' // FIXME: exoplayer dependencies kept for Video, but we shall migrate them at some point
|
||||
implementation 'com.google.android.exoplayer:exoplayer-ui:2.19.1'
|
||||
implementation "androidx.media3:media3-exoplayer:$media3_version"
|
||||
implementation "androidx.media3:media3-session:$media3_version"
|
||||
implementation "androidx.media3:media3-ui:$media3_version"
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
|
||||
implementation 'com.google.zxing:core:3.3.0' // fixed version to support SDK<24
|
||||
implementation ('com.journeyapps:zxing-android-embedded:4.3.0') { transitive = false } // QR Code scanner
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||
|
||||
<!-- force compiling emojipicker on sdk<21; runtime checks are required then -->
|
||||
<uses-sdk tools:overrideLibrary="androidx.emoji2.emojipicker"/>
|
||||
@@ -391,6 +392,15 @@
|
||||
android:name=".service.FetchForegroundService"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<service
|
||||
android:name=".service.AudioPlaybackService"
|
||||
android:foregroundServiceType="mediaPlayback"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="androidx.media3.session.MediaSessionService"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<receiver android:name=".notifications.MarkReadReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.MenuItem;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentStatePagerAdapter;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.media3.session.MediaController;
|
||||
import androidx.media3.session.SessionCommand;
|
||||
import androidx.media3.session.SessionToken;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
import com.b44t.messenger.DcChat;
|
||||
@@ -18,11 +26,13 @@ import com.b44t.messenger.DcContext;
|
||||
import com.b44t.messenger.DcEvent;
|
||||
import com.b44t.messenger.DcMsg;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
|
||||
import org.thoughtcrime.securesms.connect.DcEventCenter;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.service.AudioPlaybackService;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -30,6 +40,7 @@ import java.util.ArrayList;
|
||||
public class AllMediaActivity extends PassphraseRequiredActionBarActivity
|
||||
implements DcEventCenter.DcEventDelegate
|
||||
{
|
||||
private static final String TAG = AllMediaActivity.class.getSimpleName();
|
||||
|
||||
public static final String CHAT_ID_EXTRA = "chat_id";
|
||||
public static final String CONTACT_ID_EXTRA = "contact_id";
|
||||
@@ -57,6 +68,10 @@ public class AllMediaActivity extends PassphraseRequiredActionBarActivity
|
||||
private TabLayout tabLayout;
|
||||
private ViewPager viewPager;
|
||||
|
||||
private @Nullable MediaController mediaController;
|
||||
private ListenableFuture<MediaController> mediaControllerFuture;
|
||||
private AudioPlaybackViewModel playbackViewModel;
|
||||
|
||||
@Override
|
||||
protected void onPreCreate() {
|
||||
dynamicTheme = new DynamicNoActionBarTheme();
|
||||
@@ -91,11 +106,19 @@ public class AllMediaActivity extends PassphraseRequiredActionBarActivity
|
||||
DcEventCenter eventCenter = DcHelper.getEventCenter(this);
|
||||
eventCenter.addObserver(DcContext.DC_EVENT_CHAT_MODIFIED, this);
|
||||
eventCenter.addObserver(DcContext.DC_EVENT_CONTACTS_CHANGED, this);
|
||||
|
||||
playbackViewModel = new ViewModelProvider(this).get(AudioPlaybackViewModel.class);
|
||||
initializeMediaController();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
DcHelper.getEventCenter(this).removeObservers(this);
|
||||
if (mediaController != null) {
|
||||
MediaController.releaseFuture(mediaControllerFuture);
|
||||
mediaController = null;
|
||||
playbackViewModel.setMediaController(null);
|
||||
}
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@@ -124,6 +147,40 @@ public class AllMediaActivity extends PassphraseRequiredActionBarActivity
|
||||
this.tabLayout = ViewUtil.findById(this, R.id.tab_layout);
|
||||
}
|
||||
|
||||
private void initializeMediaController() {
|
||||
SessionToken sessionToken = new SessionToken(this,
|
||||
new ComponentName(this, AudioPlaybackService.class));
|
||||
mediaControllerFuture = new MediaController.Builder(this, sessionToken)
|
||||
.buildAsync();
|
||||
mediaControllerFuture.addListener(() -> {
|
||||
try {
|
||||
mediaController = mediaControllerFuture.get();
|
||||
addActivityContext(
|
||||
this.getIntent().getExtras(),
|
||||
this.getClass().getName()
|
||||
);
|
||||
playbackViewModel.setMediaController(mediaController);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error connecting to audio playback service", e);
|
||||
}
|
||||
}, ContextCompat.getMainExecutor(this));
|
||||
}
|
||||
|
||||
private void addActivityContext(Bundle extras, String activityClassName) {
|
||||
if (mediaController == null) return;
|
||||
|
||||
Bundle commandArgs = new Bundle();
|
||||
commandArgs.putString("activity_class", activityClassName);
|
||||
if (extras != null) {
|
||||
commandArgs.putAll(extras);
|
||||
}
|
||||
|
||||
SessionCommand updateContextCommand =
|
||||
new SessionCommand("UPDATE_ACTIVITY_CONTEXT", Bundle.EMPTY);
|
||||
|
||||
mediaController.sendCustomCommand(updateContextCommand, commandArgs);
|
||||
}
|
||||
|
||||
private boolean isGlobalGallery() {
|
||||
return contactId==0 && chatId==0;
|
||||
}
|
||||
|
||||
@@ -11,9 +11,10 @@ import androidx.annotation.NonNull;
|
||||
import com.b44t.messenger.DcMsg;
|
||||
import com.codewaves.stickyheadergrid.StickyHeaderGridAdapter;
|
||||
|
||||
import org.thoughtcrime.securesms.components.AudioView;
|
||||
import org.thoughtcrime.securesms.components.DocumentView;
|
||||
import org.thoughtcrime.securesms.components.WebxdcView;
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioView;
|
||||
import org.thoughtcrime.securesms.database.loaders.BucketedThreadMediaLoader.BucketedThreadMedia;
|
||||
import org.thoughtcrime.securesms.mms.AudioSlide;
|
||||
import org.thoughtcrime.securesms.mms.DocumentSlide;
|
||||
@@ -31,7 +32,8 @@ class AllMediaDocumentsAdapter extends StickyHeaderGridAdapter {
|
||||
private final ItemClickListener itemClickListener;
|
||||
private final Set<DcMsg> selected;
|
||||
|
||||
private BucketedThreadMedia media;
|
||||
private BucketedThreadMedia media;
|
||||
private AudioPlaybackViewModel playbackViewModel;
|
||||
|
||||
private static class ViewHolder extends StickyHeaderGridAdapter.ItemViewHolder {
|
||||
private final DocumentView documentView;
|
||||
@@ -71,6 +73,10 @@ class AllMediaDocumentsAdapter extends StickyHeaderGridAdapter {
|
||||
this.media = media;
|
||||
}
|
||||
|
||||
public void setPlaybackViewModel(AudioPlaybackViewModel playbackViewModel) {
|
||||
this.playbackViewModel = playbackViewModel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StickyHeaderGridAdapter.HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent, int headerType) {
|
||||
return new HeaderHolder(LayoutInflater.from(context).inflate(R.layout.contact_selection_list_divider, parent, false));
|
||||
@@ -97,6 +103,7 @@ class AllMediaDocumentsAdapter extends StickyHeaderGridAdapter {
|
||||
viewHolder.webxdcView.setVisibility(View.GONE);
|
||||
|
||||
viewHolder.audioView.setVisibility(View.VISIBLE);
|
||||
viewHolder.audioView.setPlaybackViewModel(playbackViewModel);
|
||||
viewHolder.audioView.setAudio((AudioSlide)slide, dcMsg.getDuration());
|
||||
viewHolder.audioView.setOnClickListener(view -> itemClickListener.onMediaClicked(dcMsg));
|
||||
viewHolder.audioView.setOnLongClickListener(view -> { itemClickListener.onMediaLongClicked(dcMsg); return true; });
|
||||
|
||||
@@ -16,6 +16,7 @@ import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
@@ -25,6 +26,7 @@ import com.b44t.messenger.DcEvent;
|
||||
import com.b44t.messenger.DcMsg;
|
||||
import com.codewaves.stickyheadergrid.StickyHeaderGridLayoutManager;
|
||||
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
|
||||
import org.thoughtcrime.securesms.connect.DcEventCenter;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.database.loaders.BucketedThreadMediaLoader;
|
||||
@@ -72,9 +74,11 @@ public class AllMediaDocumentsFragment
|
||||
// add padding to avoid content hidden behind system bars
|
||||
ViewUtil.applyWindowInsets(recyclerView, true, false, true, true);
|
||||
|
||||
this.recyclerView.setAdapter(new AllMediaDocumentsAdapter(getContext(),
|
||||
new BucketedThreadMediaLoader.BucketedThreadMedia(getContext()),
|
||||
this));
|
||||
AllMediaDocumentsAdapter adapter = new AllMediaDocumentsAdapter(getContext(),
|
||||
new BucketedThreadMediaLoader.BucketedThreadMedia(getContext()),
|
||||
this);
|
||||
this.recyclerView.setAdapter(adapter);
|
||||
adapter.setPlaybackViewModel(new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class));
|
||||
this.recyclerView.setLayoutManager(gridManager);
|
||||
this.recyclerView.setHasFixedSize(true);
|
||||
|
||||
@@ -239,12 +243,13 @@ public class AllMediaDocumentsFragment
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem menuItem) {
|
||||
int itemId = menuItem.getItemId();
|
||||
AudioPlaybackViewModel playbackViewModel = new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class);
|
||||
if (itemId == R.id.details) {
|
||||
handleDisplayDetails(getSelectedMessageRecord(getListAdapter().getSelectedMedia()));
|
||||
mode.finish();
|
||||
return true;
|
||||
} else if (itemId == R.id.delete) {
|
||||
handleDeleteMessages(chatId, getListAdapter().getSelectedMedia());
|
||||
handleDeleteMessages(chatId, getListAdapter().getSelectedMedia(), playbackViewModel::stopByIds, playbackViewModel::stopByIds);
|
||||
mode.finish();
|
||||
return true;
|
||||
} else if (itemId == R.id.share) {
|
||||
|
||||
@@ -50,11 +50,11 @@ public abstract class BaseConversationItem extends LinearLayout
|
||||
this.rpc = DcHelper.getRpc(context);
|
||||
}
|
||||
|
||||
protected void bind(@NonNull DcMsg messageRecord,
|
||||
@NonNull DcChat dcChat,
|
||||
@NonNull Set<DcMsg> batchSelected,
|
||||
boolean pulseHighlight,
|
||||
@NonNull Recipient conversationRecipient)
|
||||
protected void bindPartial(@NonNull DcMsg messageRecord,
|
||||
@NonNull DcChat dcChat,
|
||||
@NonNull Set<DcMsg> batchSelected,
|
||||
boolean pulseHighlight,
|
||||
@NonNull Recipient conversationRecipient)
|
||||
{
|
||||
this.messageRecord = messageRecord;
|
||||
this.dcChat = dcChat;
|
||||
@@ -126,6 +126,8 @@ public abstract class BaseConversationItem extends LinearLayout
|
||||
|
||||
public void onClick(View v) {
|
||||
if (!shouldInterceptClicks(messageRecord) && parent != null) {
|
||||
// The click workaround on ConversationItem shall be revised.
|
||||
// In fact, it is probably better rethinking accessibility approach for the items.
|
||||
if (batchSelected.isEmpty() && Util.isTouchExplorationEnabled(context)) {
|
||||
BaseConversationItem.this.onAccessibilityClick();
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import androidx.annotation.Nullable;
|
||||
import com.b44t.messenger.DcChat;
|
||||
import com.b44t.messenger.DcMsg;
|
||||
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioView;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
@@ -17,7 +19,9 @@ public interface BindableConversationItem extends Unbindable {
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull Set<DcMsg> batchSelected,
|
||||
@NonNull Recipient recipients,
|
||||
boolean pulseHighlight);
|
||||
boolean pulseHighlight,
|
||||
@Nullable AudioPlaybackViewModel playbackViewModel,
|
||||
AudioView.OnActionListener audioPlayPauseListener);
|
||||
|
||||
DcMsg getMessageRecord();
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.ClipData;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.res.Configuration;
|
||||
@@ -65,7 +66,12 @@ import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.SearchView;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.view.WindowCompat;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.media3.session.MediaController;
|
||||
import androidx.media3.session.SessionCommand;
|
||||
import androidx.media3.session.SessionToken;
|
||||
|
||||
import com.b44t.messenger.DcChat;
|
||||
import com.b44t.messenger.DcContact;
|
||||
@@ -76,7 +82,7 @@ import com.b44t.messenger.DcMsg;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.UriAttachment;
|
||||
import org.thoughtcrime.securesms.audio.AudioRecorder;
|
||||
import org.thoughtcrime.securesms.audio.AudioSlidePlayer;
|
||||
import org.thoughtcrime.securesms.calls.CallUtil;
|
||||
import org.thoughtcrime.securesms.components.AnimatingToggle;
|
||||
import org.thoughtcrime.securesms.components.AttachmentTypeSelector;
|
||||
import org.thoughtcrime.securesms.components.ComposeText;
|
||||
@@ -86,6 +92,8 @@ import org.thoughtcrime.securesms.components.InputPanel;
|
||||
import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout.OnKeyboardShownListener;
|
||||
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.emoji.MediaKeyboard;
|
||||
import org.thoughtcrime.securesms.connect.AccountManager;
|
||||
import org.thoughtcrime.securesms.connect.DcEventCenter;
|
||||
@@ -104,19 +112,19 @@ import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.scribbles.ScribbleActivity;
|
||||
import org.thoughtcrime.securesms.service.AudioPlaybackService;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.Prefs;
|
||||
import org.thoughtcrime.securesms.util.ShareUtil;
|
||||
import org.thoughtcrime.securesms.util.SendRelayedMessageUtil;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.ShareUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
|
||||
import org.thoughtcrime.securesms.util.guava.Optional;
|
||||
import org.thoughtcrime.securesms.util.views.ProgressDialog;
|
||||
import org.thoughtcrime.securesms.video.recode.VideoRecoder;
|
||||
import org.thoughtcrime.securesms.calls.CallUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
@@ -138,14 +146,13 @@ import chat.delta.util.SettableFuture;
|
||||
*/
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
implements ConversationFragment.ConversationFragmentListener,
|
||||
AttachmentManager.AttachmentListener,
|
||||
SearchView.OnQueryTextListener,
|
||||
DcEventCenter.DcEventDelegate,
|
||||
OnKeyboardShownListener,
|
||||
InputPanel.Listener,
|
||||
InputPanel.MediaListener
|
||||
{
|
||||
implements ConversationFragment.ConversationFragmentListener,
|
||||
AttachmentManager.AttachmentListener,
|
||||
SearchView.OnQueryTextListener,
|
||||
DcEventCenter.DcEventDelegate,
|
||||
OnKeyboardShownListener,
|
||||
InputPanel.Listener,
|
||||
InputPanel.MediaListener, AudioView.OnActionListener {
|
||||
private static final String TAG = ConversationActivity.class.getSimpleName();
|
||||
|
||||
public static final String ACCOUNT_ID_EXTRA = "account_id";
|
||||
@@ -185,6 +192,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
private MediaKeyboard emojiPicker;
|
||||
protected HidingLinearLayout quickAttachmentToggle;
|
||||
private InputPanel inputPanel;
|
||||
private @Nullable MediaController mediaController;
|
||||
private com.google.common.util.concurrent.ListenableFuture<MediaController> mediaControllerFuture;
|
||||
private AudioPlaybackViewModel playbackViewModel;
|
||||
|
||||
private ApplicationContext context;
|
||||
private Recipient recipient;
|
||||
@@ -217,6 +227,10 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
initializeActionBar();
|
||||
initializeViews();
|
||||
initializeResources();
|
||||
|
||||
playbackViewModel = new ViewModelProvider(this).get(AudioPlaybackViewModel.class);
|
||||
initializeMediaController();
|
||||
|
||||
initializeSecurity(false, isDefaultSms).addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean result) {
|
||||
@@ -267,6 +281,36 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeMediaController() {
|
||||
SessionToken sessionToken = new SessionToken(this,
|
||||
new ComponentName(this, AudioPlaybackService.class));
|
||||
mediaControllerFuture = new MediaController.Builder(this, sessionToken)
|
||||
.buildAsync();
|
||||
mediaControllerFuture.addListener(() -> {
|
||||
try {
|
||||
mediaController = mediaControllerFuture.get();
|
||||
playbackViewModel.setMediaController(mediaController);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error connecting to audio playback service", e);
|
||||
}
|
||||
}, ContextCompat.getMainExecutor(this));
|
||||
}
|
||||
|
||||
private void addActivityContext(Bundle extras, String activityClassName) {
|
||||
if (mediaController == null) return;
|
||||
|
||||
Bundle commandArgs = new Bundle();
|
||||
commandArgs.putString("activity_class", activityClassName);
|
||||
if (extras != null) {
|
||||
commandArgs.putAll(extras);
|
||||
}
|
||||
|
||||
SessionCommand updateContextCommand =
|
||||
new SessionCommand("UPDATE_ACTIVITY_CONTEXT", Bundle.EMPTY);
|
||||
|
||||
mediaController.sendCustomCommand(updateContextCommand, commandArgs);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
@@ -337,7 +381,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
DcHelper.getNotificationCenter(this).clearVisibleChat();
|
||||
if (isFinishing()) overridePendingTransition(R.anim.fade_scale_in, R.anim.slide_to_right);
|
||||
inputPanel.onPause();
|
||||
AudioSlidePlayer.stopAll();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -357,6 +400,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
DcHelper.getEventCenter(this).removeObservers(this);
|
||||
if (mediaController != null) {
|
||||
MediaController.releaseFuture(mediaControllerFuture);
|
||||
mediaController = null;
|
||||
playbackViewModel.setMediaController(null);
|
||||
}
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@@ -634,6 +682,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
extras.putInt(ConversationListFragment.RELOAD_LIST, 1);
|
||||
}
|
||||
|
||||
playbackViewModel.stopNonMessageAudioPlayback();
|
||||
|
||||
boolean archived = getIntent().getBooleanExtra(FROM_ARCHIVED_CHATS_EXTRA, false);
|
||||
Intent intent = new Intent(this, (archived ? ConversationListArchiveActivity.class : ConversationListActivity.class));
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
@@ -1007,20 +1057,26 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
recipient = new Recipient(this, dcChat);
|
||||
glideRequests = GlideApp.with(this);
|
||||
|
||||
setComposePanelVisibility();
|
||||
setComposePanelVisibility(true);
|
||||
initializeContactRequest();
|
||||
}
|
||||
|
||||
private void setComposePanelVisibility() {
|
||||
private void setComposePanelVisibility(boolean isInitialization) {
|
||||
if (dcChat.canSend()) {
|
||||
composePanel.setVisibility(View.VISIBLE);
|
||||
attachmentManager.setHidden(false);
|
||||
inputPanel.setSubjectVisible(!dcChat.isEncrypted());
|
||||
ViewUtil.forceApplyWindowInsets(findViewById(R.id.root_layout), true, false, true, true);
|
||||
fragment.handleRemoveBottomInsets();
|
||||
} else {
|
||||
composePanel.setVisibility(View.GONE);
|
||||
attachmentManager.setHidden(true);
|
||||
hideSoftKeyboard();
|
||||
inputPanel.setSubjectVisible(false);
|
||||
if (isInitialization) {
|
||||
ViewUtil.forceApplyWindowInsets(findViewById(R.id.root_layout), true, false, true, false);
|
||||
fragment.handleAddBottomInsets();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1057,11 +1113,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
return new SettableFuture<>(false);
|
||||
}
|
||||
|
||||
return attachmentManager.setMedia(glideRequests, uri, null, mediaType, 0, 0, chatId);
|
||||
return attachmentManager.setMedia(glideRequests, uri, null, mediaType, 0, 0, chatId, playbackViewModel);
|
||||
}
|
||||
|
||||
private ListenableFuture<Boolean> setMedia(DcMsg msg, @NonNull MediaType mediaType) {
|
||||
return attachmentManager.setMedia(glideRequests, Uri.fromFile(new File(msg.getFile())), msg, mediaType, 0, 0, chatId);
|
||||
return attachmentManager.setMedia(glideRequests, Uri.fromFile(new File(msg.getFile())), msg, mediaType, 0, 0, chatId, playbackViewModel);
|
||||
}
|
||||
|
||||
private void addAttachmentContactInfo(int contactId) {
|
||||
@@ -1113,6 +1169,10 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
inputPanel.clearSubject();
|
||||
}
|
||||
|
||||
// Stop draft audio playback regardless, since it is unlikely
|
||||
// we will need background playback for drafts
|
||||
playbackViewModel.stopNonMessageAudioPlayback();
|
||||
|
||||
DcContext dcContext = DcHelper.getContext(context);
|
||||
Util.runOnAnyBackgroundThread(() -> {
|
||||
DcMsg msg = null;
|
||||
@@ -1425,6 +1485,14 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
// Listeners
|
||||
|
||||
@Override
|
||||
public void onPlayPauseButtonClicked(View view) {
|
||||
addActivityContext(
|
||||
this.getIntent().getExtras(),
|
||||
this.getClass().getName()
|
||||
);
|
||||
}
|
||||
|
||||
private class AttachmentTypeListener implements AttachmentTypeSelector.AttachmentClickedListener {
|
||||
@Override
|
||||
public void onClick(int type) {
|
||||
@@ -1593,7 +1661,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
dcChat = dcContext.getChat(chatId);
|
||||
titleView.setTitle(glideRequests, dcChat);
|
||||
initializeSecurity(isSecureText, isDefaultSms);
|
||||
setComposePanelVisibility();
|
||||
setComposePanelVisibility(false);
|
||||
initializeContactRequest();
|
||||
} else if ((eventId == DcContext.DC_EVENT_INCOMING_MSG
|
||||
|| eventId == DcContext.DC_EVENT_MSG_READ)
|
||||
|
||||
@@ -34,6 +34,8 @@ import com.b44t.messenger.DcContext;
|
||||
import com.b44t.messenger.DcMsg;
|
||||
|
||||
import org.thoughtcrime.securesms.ConversationAdapter.HeaderViewHolder;
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioView;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
@@ -59,6 +61,7 @@ import java.util.Set;
|
||||
* @author Moxie Marlinspike
|
||||
*
|
||||
*/
|
||||
// FIXME: this breaks type checks, that is why there are so many casts.
|
||||
public class ConversationAdapter <V extends View & BindableConversationItem>
|
||||
extends RecyclerView.Adapter
|
||||
implements StickyHeaderDecoration.StickyHeaderAdapter<HeaderViewHolder>
|
||||
@@ -97,6 +100,8 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
||||
private long pulseHighlightingSince = -1;
|
||||
private int lastSeenPosition = -1;
|
||||
private long lastSeen = -1;
|
||||
private AudioPlaybackViewModel playbackViewModel;
|
||||
private AudioView.OnActionListener audioPlayPauseListener;
|
||||
|
||||
protected static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
public <V extends View & BindableConversationItem> ViewHolder(final @NonNull V itemView) {
|
||||
@@ -170,6 +175,14 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
||||
return fromDb;
|
||||
}
|
||||
|
||||
public void setPlaybackViewModel(AudioPlaybackViewModel playbackViewModel) {
|
||||
this.playbackViewModel = playbackViewModel;
|
||||
}
|
||||
|
||||
public void setAudioPlayPauseListener(AudioView.OnActionListener audioPlayPauseListener) {
|
||||
this.audioPlayPauseListener = audioPlayPauseListener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the position of the message with msgId in the chat list, counted from the top
|
||||
*/
|
||||
@@ -237,7 +250,7 @@ public class ConversationAdapter <V extends View & BindableConversationItem>
|
||||
long elapsed = now - pulseHighlightingSince;
|
||||
boolean pulseHighlight = (positionCurrentlyPulseHighlighting == position && elapsed < PULSE_HIGHLIGHT_MILLIS);
|
||||
|
||||
holder.getItem().bind(getMsg(position), dcChat, glideRequests, batchSelected, recipient, pulseHighlight);
|
||||
holder.getItem().bind(getMsg(position), dcChat, glideRequests, batchSelected, recipient, pulseHighlight, playbackViewModel, audioPlayPauseListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -41,6 +41,7 @@ import android.widget.Toast;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.RecyclerView.OnScrollListener;
|
||||
@@ -52,6 +53,7 @@ import com.b44t.messenger.DcEvent;
|
||||
import com.b44t.messenger.DcMsg;
|
||||
|
||||
import org.thoughtcrime.securesms.ConversationAdapter.ItemClickListener;
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
|
||||
import org.thoughtcrime.securesms.components.reminder.DozeReminder;
|
||||
import org.thoughtcrime.securesms.connect.DcEventCenter;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
@@ -101,6 +103,7 @@ public class ConversationFragment extends MessageSelectorFragment
|
||||
private StickyHeaderDecoration dateDecoration;
|
||||
private View scrollToBottomButton;
|
||||
private View floatingLocationButton;
|
||||
private View bottomDivider;
|
||||
private AddReactionView addReactionView;
|
||||
private TextView noMessageTextView;
|
||||
private Timer reloadTimer;
|
||||
@@ -108,6 +111,8 @@ public class ConversationFragment extends MessageSelectorFragment
|
||||
public boolean isPaused;
|
||||
private Debouncer markseenDebouncer;
|
||||
private Rpc rpc;
|
||||
private boolean pendingAddBottomInsets;
|
||||
private boolean pendingRemoveBottomInsets;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle icicle) {
|
||||
@@ -141,6 +146,7 @@ public class ConversationFragment extends MessageSelectorFragment
|
||||
floatingLocationButton = ViewUtil.findById(view, R.id.floating_location_button);
|
||||
addReactionView = ViewUtil.findById(view, R.id.add_reaction_view);
|
||||
noMessageTextView = ViewUtil.findById(view, R.id.no_messages_text_view);
|
||||
bottomDivider = ViewUtil.findById(view, R.id.bottom_divider);
|
||||
|
||||
scrollToBottomButton.setOnClickListener(v -> scrollToBottom());
|
||||
|
||||
@@ -159,18 +165,32 @@ public class ConversationFragment extends MessageSelectorFragment
|
||||
// with hardware layers, drawing may result in errors as "OpenGLRenderer: Path too large to be rendered into a texture"
|
||||
list.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
|
||||
|
||||
if (pendingAddBottomInsets) {
|
||||
bottomDivider.setVisibility(View.GONE);
|
||||
ViewUtil.forceApplyWindowInsets(list, false, true, false, true);
|
||||
ViewUtil.forceApplyWindowInsetsAsMargin(scrollToBottomButton, true, true, true, true);
|
||||
pendingAddBottomInsets = false;
|
||||
}
|
||||
|
||||
if (pendingRemoveBottomInsets) {
|
||||
bottomDivider.setVisibility(View.VISIBLE);
|
||||
ViewUtil.forceApplyWindowInsets(list, false, true, false, false);
|
||||
ViewUtil.forceApplyWindowInsetsAsMargin(scrollToBottomButton, true, true, true, false);
|
||||
pendingRemoveBottomInsets = false;
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle bundle) {
|
||||
super.onActivityCreated(bundle);
|
||||
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
initializeResources();
|
||||
initializeListAdapter();
|
||||
}
|
||||
|
||||
private void setNoMessageText() {
|
||||
private void setNoMessageText() {
|
||||
DcChat dcChat = getListAdapter().getChat();
|
||||
if(dcChat.isMultiUser()){
|
||||
if (dcChat.isInBroadcast() || dcChat.isOutBroadcast()) {
|
||||
@@ -197,6 +217,28 @@ public class ConversationFragment extends MessageSelectorFragment
|
||||
}
|
||||
}
|
||||
|
||||
public void handleAddBottomInsets() {
|
||||
if (bottomDivider != null) {
|
||||
bottomDivider.setVisibility(View.GONE);
|
||||
ViewUtil.forceApplyWindowInsets(list, false, true, false, true);
|
||||
ViewUtil.forceApplyWindowInsetsAsMargin(scrollToBottomButton, false, false, false, true);
|
||||
pendingAddBottomInsets = false;
|
||||
} else {
|
||||
pendingAddBottomInsets = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void handleRemoveBottomInsets() {
|
||||
if (bottomDivider != null) {
|
||||
bottomDivider.setVisibility(View.VISIBLE);
|
||||
ViewUtil.forceApplyWindowInsets(list, false, true, false, false);
|
||||
ViewUtil.forceApplyWindowInsetsAsMargin(scrollToBottomButton, false, false, false, false);
|
||||
pendingRemoveBottomInsets = false;
|
||||
} else {
|
||||
pendingRemoveBottomInsets = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
DcHelper.getEventCenter(getContext()).removeObservers(this);
|
||||
@@ -291,6 +333,10 @@ public class ConversationFragment extends MessageSelectorFragment
|
||||
if (this.recipient != null && this.chatId != -1) {
|
||||
ConversationAdapter adapter = new ConversationAdapter(getActivity(), this.recipient.getChat(), GlideApp.with(this), selectionClickListener, this.recipient);
|
||||
list.setAdapter(adapter);
|
||||
AudioPlaybackViewModel playbackViewModel =
|
||||
new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class);
|
||||
adapter.setPlaybackViewModel(playbackViewModel);
|
||||
adapter.setAudioPlayPauseListener(((ConversationActivity) requireActivity()));
|
||||
|
||||
if (dateDecoration != null) {
|
||||
list.removeItemDecoration(dateDecoration);
|
||||
@@ -407,7 +453,14 @@ public class ConversationFragment extends MessageSelectorFragment
|
||||
}
|
||||
|
||||
public void handleClearChat() {
|
||||
handleDeleteMessages((int) chatId, getListAdapter().getMessageIds());
|
||||
AudioPlaybackViewModel playbackViewModel =
|
||||
new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class);
|
||||
|
||||
handleDeleteMessages(
|
||||
(int) chatId,
|
||||
getListAdapter().getMessageIds(),
|
||||
playbackViewModel::stopByIds,
|
||||
playbackViewModel::stopByIds);
|
||||
}
|
||||
|
||||
private ConversationAdapter getListAdapter() {
|
||||
@@ -940,12 +993,15 @@ public class ConversationFragment extends MessageSelectorFragment
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||
hideAddReactionView();
|
||||
int itemId = item.getItemId();
|
||||
AudioPlaybackViewModel playbackViewModel =
|
||||
new ViewModelProvider(requireActivity()).get(AudioPlaybackViewModel.class);
|
||||
|
||||
if (itemId == R.id.menu_context_copy) {
|
||||
handleCopyMessage(getListAdapter().getSelectedItems());
|
||||
actionMode.finish();
|
||||
return true;
|
||||
} else if (itemId == R.id.menu_context_delete_message) {
|
||||
handleDeleteMessages((int) chatId, getListAdapter().getSelectedItems());
|
||||
handleDeleteMessages((int) chatId, getListAdapter().getSelectedItems(), playbackViewModel::stopByIds, playbackViewModel::stopByIds);
|
||||
return true;
|
||||
} else if (itemId == R.id.menu_context_share) {
|
||||
DcHelper.openForViewOrShare(getContext(), getSelectedMessageRecord(getListAdapter().getSelectedItems()).getId(), Intent.ACTION_SEND);
|
||||
|
||||
@@ -41,8 +41,7 @@ import com.b44t.messenger.DcChat;
|
||||
import com.b44t.messenger.DcContact;
|
||||
import com.b44t.messenger.DcMsg;
|
||||
|
||||
import org.thoughtcrime.securesms.audio.AudioSlidePlayer;
|
||||
import org.thoughtcrime.securesms.components.AudioView;
|
||||
import org.thoughtcrime.securesms.calls.CallUtil;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.components.BorderlessImageView;
|
||||
import org.thoughtcrime.securesms.components.CallItemView;
|
||||
@@ -52,6 +51,8 @@ import org.thoughtcrime.securesms.components.DocumentView;
|
||||
import org.thoughtcrime.securesms.components.QuoteView;
|
||||
import org.thoughtcrime.securesms.components.VcardView;
|
||||
import org.thoughtcrime.securesms.components.WebxdcView;
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioView;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.mms.AudioSlide;
|
||||
import org.thoughtcrime.securesms.mms.DocumentSlide;
|
||||
@@ -71,7 +72,6 @@ import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.views.Stub;
|
||||
import org.thoughtcrime.securesms.calls.CallUtil;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
@@ -181,9 +181,11 @@ public class ConversationItem extends BaseConversationItem
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull Set<DcMsg> batchSelected,
|
||||
@NonNull Recipient recipients,
|
||||
boolean pulseHighlight)
|
||||
boolean pulseHighlight,
|
||||
@Nullable AudioPlaybackViewModel playbackViewModel,
|
||||
AudioView.OnActionListener audioPlayPauseListener)
|
||||
{
|
||||
bind(messageRecord, dcChat, batchSelected, pulseHighlight, recipients);
|
||||
bindPartial(messageRecord, dcChat, batchSelected, pulseHighlight, recipients);
|
||||
this.glideRequests = glideRequests;
|
||||
this.showSender = ((dcChat.isMultiUser() || dcChat.isSelfTalk()) && !messageRecord.isOutgoing()) || messageRecord.getOverrideSenderName() != null;
|
||||
|
||||
@@ -204,7 +206,7 @@ public class ConversationItem extends BaseConversationItem
|
||||
|
||||
setGutterSizes(messageRecord, showSender);
|
||||
setMessageShape(messageRecord);
|
||||
setMediaAttributes(messageRecord, showSender);
|
||||
setMediaAttributes(messageRecord, showSender, playbackViewModel, audioPlayPauseListener);
|
||||
setBodyText(messageRecord);
|
||||
setBubbleState(messageRecord);
|
||||
setContactPhoto();
|
||||
@@ -482,24 +484,10 @@ public class ConversationItem extends BaseConversationItem
|
||||
}
|
||||
|
||||
private void setMediaAttributes(@NonNull DcMsg messageRecord,
|
||||
boolean showSender)
|
||||
boolean showSender,
|
||||
AudioPlaybackViewModel playbackViewModel,
|
||||
AudioView.OnActionListener audioPlayPauseListener)
|
||||
{
|
||||
class SetDurationListener implements AudioSlidePlayer.Listener {
|
||||
@Override
|
||||
public void onStart() {}
|
||||
|
||||
@Override
|
||||
public void onStop() {}
|
||||
|
||||
@Override
|
||||
public void onProgress(AudioSlide slide, double progress, long millis) {}
|
||||
|
||||
@Override
|
||||
public void onReceivedDuration(int millis) {
|
||||
messageRecord.lateFilingMediaSize(0,0, millis);
|
||||
audioViewStub.get().setDuration(millis);
|
||||
}
|
||||
}
|
||||
if (hasAudio(messageRecord)) {
|
||||
audioViewStub.get().setVisibility(View.VISIBLE);
|
||||
if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
|
||||
@@ -511,12 +499,9 @@ public class ConversationItem extends BaseConversationItem
|
||||
|
||||
//noinspection ConstantConditions
|
||||
int duration = messageRecord.getDuration();
|
||||
if (duration == 0) {
|
||||
AudioSlide audio = new AudioSlide(context, messageRecord);
|
||||
AudioSlidePlayer audioSlidePlayer = AudioSlidePlayer.createFor(getContext(), audio, new SetDurationListener());
|
||||
audioSlidePlayer.requestDuration();
|
||||
}
|
||||
|
||||
audioViewStub.get().setPlaybackViewModel(playbackViewModel);
|
||||
audioViewStub.get().setOnActionListener(audioPlayPauseListener);
|
||||
audioViewStub.get().setAudio(new AudioSlide(context, messageRecord), duration);
|
||||
audioViewStub.get().setOnClickListener(passthroughClickListener);
|
||||
audioViewStub.get().setOnLongClickListener(passthroughClickListener);
|
||||
|
||||
@@ -15,6 +15,8 @@ import com.b44t.messenger.DcMsg;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import org.thoughtcrime.securesms.components.DeliveryStatusView;
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioView;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.JsonUtils;
|
||||
@@ -61,9 +63,11 @@ public class ConversationUpdateItem extends BaseConversationItem
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull Set<DcMsg> batchSelected,
|
||||
@NonNull Recipient conversationRecipient,
|
||||
boolean pulseUpdate)
|
||||
boolean pulseUpdate,
|
||||
@Nullable AudioPlaybackViewModel playbackViewModel,
|
||||
AudioView.OnActionListener audioPlayPauseListener)
|
||||
{
|
||||
bind(messageRecord, dcChat, batchSelected, pulseUpdate, conversationRecipient);
|
||||
bindPartial(messageRecord, dcChat, batchSelected, pulseUpdate, conversationRecipient);
|
||||
setGenericInfoRecord(messageRecord);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
@@ -26,6 +27,7 @@ import org.thoughtcrime.securesms.util.StorageUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public abstract class MessageSelectorFragment
|
||||
extends Fragment
|
||||
@@ -57,10 +59,14 @@ public abstract class MessageSelectorFragment
|
||||
}
|
||||
|
||||
protected void handleDeleteMessages(int chatId, final Set<DcMsg> messageRecords) {
|
||||
handleDeleteMessages(chatId, DcMsg.msgSetToIds(messageRecords));
|
||||
handleDeleteMessages(chatId, DcMsg.msgSetToIds(messageRecords), null, null);
|
||||
}
|
||||
|
||||
protected void handleDeleteMessages(int chatId, final int[] messageIds) {
|
||||
protected void handleDeleteMessages(int chatId, final Set<DcMsg> messageRecords, Consumer<int[]> deleteForMeListenerExtra, Consumer<int[]> deleteForAllListenerExtra) {
|
||||
handleDeleteMessages(chatId, DcMsg.msgSetToIds(messageRecords), deleteForMeListenerExtra, deleteForAllListenerExtra);
|
||||
}
|
||||
|
||||
protected void handleDeleteMessages(int chatId, final int[] messageIds, Consumer<int[]> deleteForMeListenerExtra, Consumer<int[]> deleteForAllListenerExtra) {
|
||||
DcContext dcContext = DcHelper.getContext(getContext());
|
||||
DcChat dcChat = dcContext.getChat(chatId);
|
||||
boolean canDeleteForAll = true;
|
||||
@@ -79,20 +85,24 @@ public abstract class MessageSelectorFragment
|
||||
String text = getActivity().getResources().getQuantityString(R.plurals.ask_delete_messages, messageIds.length, messageIds.length);
|
||||
int positiveBtnLabel = dcChat.isSelfTalk() ? R.string.delete : R.string.delete_for_me;
|
||||
|
||||
DialogInterface.OnClickListener deleteForMeListener = (d, which) -> {
|
||||
Util.runOnAnyBackgroundThread(() -> dcContext.deleteMsgs(messageIds));
|
||||
if (actionMode != null) actionMode.finish();
|
||||
if (deleteForMeListenerExtra != null) deleteForMeListenerExtra.accept(messageIds);
|
||||
};
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity())
|
||||
.setMessage(text)
|
||||
.setCancelable(true)
|
||||
.setNeutralButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(positiveBtnLabel, (d, which) -> {
|
||||
Util.runOnAnyBackgroundThread(() -> dcContext.deleteMsgs(messageIds));
|
||||
if (actionMode != null) actionMode.finish();
|
||||
});
|
||||
.setPositiveButton(positiveBtnLabel, deleteForMeListener);
|
||||
|
||||
if(canDeleteForAll) {
|
||||
builder.setNegativeButton(R.string.delete_for_everyone, (d, which) -> {
|
||||
DialogInterface.OnClickListener deleteForAllListener = (d, which) -> {
|
||||
Util.runOnAnyBackgroundThread(() -> dcContext.sendDeleteRequest(messageIds));
|
||||
if (actionMode != null) actionMode.finish();
|
||||
});
|
||||
if (deleteForAllListenerExtra != null) deleteForAllListenerExtra.accept(messageIds);
|
||||
};
|
||||
builder.setNegativeButton(R.string.delete_for_everyone, deleteForAllListener);
|
||||
AlertDialog dialog = builder.show();
|
||||
Util.redButton(dialog, AlertDialog.BUTTON_NEGATIVE);
|
||||
Util.redPositiveButton(dialog);
|
||||
|
||||
@@ -77,7 +77,7 @@ public class WebViewActivity extends PassphraseRequiredActionBarActivity
|
||||
findViewById(R.id.status_bar_background).setBackgroundResource(R.drawable.search_toolbar_shadow);
|
||||
} else {
|
||||
// add padding to avoid content hidden behind system bars
|
||||
ViewUtil.applyWindowInsets(findViewById(R.id.content_container), true, true, true, true, true);
|
||||
ViewUtil.applyWindowInsets(findViewById(R.id.content_container), true, true, true, true, true, false);
|
||||
}
|
||||
|
||||
webView.setWebViewClient(new WebViewClient() {
|
||||
|
||||
@@ -1,356 +0,0 @@
|
||||
package org.thoughtcrime.securesms.audio;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.DefaultLoadControl;
|
||||
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
||||
import com.google.android.exoplayer2.LoadControl;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.PlaybackException;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.audio.AudioAttributes;
|
||||
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.mms.AudioSlide;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.guava.Optional;
|
||||
import org.thoughtcrime.securesms.video.exo.AttachmentDataSourceFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
public class AudioSlidePlayer {
|
||||
|
||||
private static final String TAG = AudioSlidePlayer.class.getSimpleName();
|
||||
|
||||
private static @NonNull Optional<AudioSlidePlayer> playing = Optional.absent();
|
||||
|
||||
private final @NonNull Context context;
|
||||
private final @NonNull AudioSlide slide;
|
||||
private final @NonNull Handler progressEventHandler;
|
||||
|
||||
private @NonNull WeakReference<Listener> listener;
|
||||
private @Nullable SimpleExoPlayer mediaPlayer;
|
||||
private @Nullable SimpleExoPlayer durationCalculator;
|
||||
|
||||
public synchronized static AudioSlidePlayer createFor(@NonNull Context context,
|
||||
@NonNull AudioSlide slide,
|
||||
@NonNull Listener listener)
|
||||
{
|
||||
if (playing.isPresent() && playing.get().getAudioSlide().equals(slide)) {
|
||||
playing.get().setListener(listener);
|
||||
return playing.get();
|
||||
} else {
|
||||
return new AudioSlidePlayer(context, slide, listener);
|
||||
}
|
||||
}
|
||||
|
||||
private AudioSlidePlayer(@NonNull Context context,
|
||||
@NonNull AudioSlide slide,
|
||||
@NonNull Listener listener)
|
||||
{
|
||||
this.context = context;
|
||||
this.slide = slide;
|
||||
this.listener = new WeakReference<>(listener);
|
||||
this.progressEventHandler = new ProgressEventHandler(this);
|
||||
}
|
||||
|
||||
public void requestDuration() {
|
||||
try {
|
||||
LoadControl loadControl = new DefaultLoadControl.Builder().setBufferDurationsMs(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE).build();
|
||||
durationCalculator = new SimpleExoPlayer.Builder(context, new DefaultRenderersFactory(context))
|
||||
.setTrackSelector(new DefaultTrackSelector(context))
|
||||
.setLoadControl(loadControl)
|
||||
.build();
|
||||
durationCalculator.setPlayWhenReady(false);
|
||||
durationCalculator.addListener(new Player.Listener() {
|
||||
@Override
|
||||
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
||||
if (playbackState == Player.STATE_READY) {
|
||||
Util.runOnMain(() -> {
|
||||
synchronized (AudioSlidePlayer.this) {
|
||||
if (durationCalculator == null) return;
|
||||
Log.d(TAG, "request duration " + durationCalculator.getDuration());
|
||||
getListener().onReceivedDuration(Long.valueOf(durationCalculator.getDuration()).intValue());
|
||||
durationCalculator.release();
|
||||
durationCalculator.removeListener(this);
|
||||
durationCalculator = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
durationCalculator.prepare(createMediaSource(slide.getUri()));
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, e);
|
||||
getListener().onReceivedDuration(0);
|
||||
}
|
||||
}
|
||||
|
||||
public void play(final double progress) throws IOException {
|
||||
play(progress, false);
|
||||
}
|
||||
|
||||
private void play(final double progress, boolean earpiece) throws IOException {
|
||||
if (this.mediaPlayer != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (slide.getUri() == null) {
|
||||
throw new IOException("Slide has no URI!");
|
||||
}
|
||||
|
||||
LoadControl loadControl = new DefaultLoadControl.Builder().setBufferDurationsMs(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE).build();
|
||||
this.mediaPlayer = new SimpleExoPlayer.Builder(context, new DefaultRenderersFactory(context))
|
||||
.setTrackSelector(new DefaultTrackSelector(context))
|
||||
.setLoadControl(loadControl)
|
||||
.build();
|
||||
|
||||
mediaPlayer.prepare(createMediaSource(slide.getUri()));
|
||||
mediaPlayer.setPlayWhenReady(true);
|
||||
mediaPlayer.setAudioAttributes(new AudioAttributes.Builder()
|
||||
.setContentType(earpiece ? C.AUDIO_CONTENT_TYPE_SPEECH : C.AUDIO_CONTENT_TYPE_MUSIC)
|
||||
.setUsage(earpiece ? C.USAGE_VOICE_COMMUNICATION : C.USAGE_MEDIA)
|
||||
.build(), false);
|
||||
mediaPlayer.addListener(new Player.Listener() {
|
||||
|
||||
boolean started = false;
|
||||
|
||||
@Override
|
||||
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
||||
Log.d(TAG, "onPlayerStateChanged(" + playWhenReady + ", " + playbackState + ")");
|
||||
switch (playbackState) {
|
||||
case Player.STATE_READY:
|
||||
|
||||
Log.i(TAG, "onPrepared() " + mediaPlayer.getBufferedPercentage() + "% buffered");
|
||||
synchronized (AudioSlidePlayer.this) {
|
||||
if (mediaPlayer == null) return;
|
||||
Log.d(TAG, "DURATION: " + mediaPlayer.getDuration());
|
||||
|
||||
if (started) {
|
||||
Log.d(TAG, "Already started. Ignoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
started = true;
|
||||
|
||||
if (progress > 0) {
|
||||
mediaPlayer.seekTo((long) (mediaPlayer.getDuration() * progress));
|
||||
}
|
||||
|
||||
setPlaying(AudioSlidePlayer.this);
|
||||
}
|
||||
|
||||
keepScreenOn(true);
|
||||
notifyOnStart();
|
||||
progressEventHandler.sendEmptyMessage(0);
|
||||
break;
|
||||
|
||||
case Player.STATE_ENDED:
|
||||
Log.i(TAG, "onComplete");
|
||||
synchronized (AudioSlidePlayer.this) {
|
||||
getListener().onReceivedDuration(Long.valueOf(mediaPlayer.getDuration()).intValue());
|
||||
mediaPlayer.release();
|
||||
mediaPlayer = null;
|
||||
}
|
||||
|
||||
keepScreenOn(false);
|
||||
notifyOnStop();
|
||||
progressEventHandler.removeMessages(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerError(PlaybackException error) {
|
||||
Log.w(TAG, "MediaPlayer Error: " + error);
|
||||
|
||||
synchronized (AudioSlidePlayer.this) {
|
||||
mediaPlayer.release();
|
||||
mediaPlayer = null;
|
||||
}
|
||||
|
||||
notifyOnStop();
|
||||
progressEventHandler.removeMessages(0);
|
||||
|
||||
// Failed to play media file, maybe another app can handle it
|
||||
int msgId = getAudioSlide().getDcMsgId();
|
||||
DcHelper.openForViewOrShare(context, msgId, Intent.ACTION_VIEW);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private MediaSource createMediaSource(@NonNull Uri uri) {
|
||||
DefaultDataSourceFactory defaultDataSourceFactory = new DefaultDataSourceFactory(context, "GenericUserAgent", null);
|
||||
AttachmentDataSourceFactory attachmentDataSourceFactory = new AttachmentDataSourceFactory(defaultDataSourceFactory);
|
||||
ExtractorsFactory extractorsFactory = new DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true);
|
||||
|
||||
return new ProgressiveMediaSource.Factory(attachmentDataSourceFactory, extractorsFactory)
|
||||
.createMediaSource(MediaItem.fromUri(uri));
|
||||
}
|
||||
|
||||
public synchronized void stop() {
|
||||
Log.i(TAG, "Stop called!");
|
||||
|
||||
keepScreenOn(false);
|
||||
removePlaying(this);
|
||||
|
||||
if (this.mediaPlayer != null) {
|
||||
this.mediaPlayer.stop();
|
||||
this.mediaPlayer.release();
|
||||
}
|
||||
|
||||
this.mediaPlayer = null;
|
||||
}
|
||||
|
||||
public static void stopAll() {
|
||||
if (playing.isPresent()) {
|
||||
synchronized (AudioSlidePlayer.class) {
|
||||
if (playing.isPresent()) {
|
||||
playing.get().stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setListener(@NonNull Listener listener) {
|
||||
this.listener = new WeakReference<>(listener);
|
||||
|
||||
if (this.mediaPlayer != null && this.mediaPlayer.getPlaybackState() == Player.STATE_READY) {
|
||||
notifyOnStart();
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull AudioSlide getAudioSlide() {
|
||||
return slide;
|
||||
}
|
||||
|
||||
|
||||
private Pair<Double, Integer> getProgress() {
|
||||
if (mediaPlayer == null || mediaPlayer.getCurrentPosition() <= 0 || mediaPlayer.getDuration() <= 0) {
|
||||
return new Pair<>(0D, 0);
|
||||
} else {
|
||||
return new Pair<>((double) mediaPlayer.getCurrentPosition() / (double) mediaPlayer.getDuration(),
|
||||
(int) mediaPlayer.getCurrentPosition());
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyOnStart() {
|
||||
Util.runOnMain(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
getListener().onStart();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void notifyOnStop() {
|
||||
Util.runOnMain(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
getListener().onStop();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void notifyOnProgress(final double progress, final long millis) {
|
||||
Util.runOnMain(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
getListener().onProgress(slide, progress, millis);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private @NonNull Listener getListener() {
|
||||
Listener listener = this.listener.get();
|
||||
|
||||
if (listener != null) return listener;
|
||||
else return new Listener() {
|
||||
@Override
|
||||
public void onStart() {}
|
||||
@Override
|
||||
public void onStop() {}
|
||||
@Override
|
||||
public void onProgress(AudioSlide slide, double progress, long millis) {}
|
||||
@Override
|
||||
public void onReceivedDuration(int millis) {}
|
||||
};
|
||||
}
|
||||
|
||||
public void keepScreenOn(boolean keepOn) {
|
||||
if (context instanceof Activity) {
|
||||
if (keepOn) {
|
||||
((Activity) context).getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
} else {
|
||||
((Activity) context).getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized static void setPlaying(@NonNull AudioSlidePlayer player) {
|
||||
if (playing.isPresent() && playing.get() != player) {
|
||||
playing.get().notifyOnStop();
|
||||
playing.get().stop();
|
||||
}
|
||||
|
||||
playing = Optional.of(player);
|
||||
}
|
||||
|
||||
private synchronized static void removePlaying(@NonNull AudioSlidePlayer player) {
|
||||
if (playing.isPresent() && playing.get() == player) {
|
||||
playing = Optional.absent();
|
||||
}
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onStart();
|
||||
void onStop();
|
||||
void onProgress(AudioSlide slide, double progress, long millis);
|
||||
void onReceivedDuration(int millis);
|
||||
}
|
||||
|
||||
private static class ProgressEventHandler extends Handler {
|
||||
|
||||
private final WeakReference<AudioSlidePlayer> playerReference;
|
||||
|
||||
private ProgressEventHandler(@NonNull AudioSlidePlayer player) {
|
||||
this.playerReference = new WeakReference<>(player);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(@NonNull Message msg) {
|
||||
AudioSlidePlayer player = playerReference.get();
|
||||
|
||||
if (player == null || player.mediaPlayer == null || !isPlayerActive(player.mediaPlayer)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Pair<Double, Integer> progress = player.getProgress();
|
||||
player.notifyOnProgress(progress.first, progress.second);
|
||||
sendEmptyMessageDelayed(0, 50);
|
||||
}
|
||||
|
||||
private boolean isPlayerActive(@NonNull SimpleExoPlayer player) {
|
||||
return player.getPlaybackState() == Player.STATE_READY || player.getPlaybackState() == Player.STATE_BUFFERING;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,301 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.AnimatedVectorDrawable;
|
||||
import android.media.AudioAttributes;
|
||||
import android.media.AudioFocusRequest;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Build;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.audio.AudioSlidePlayer;
|
||||
import org.thoughtcrime.securesms.mms.AudioSlide;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
|
||||
public class AudioView extends FrameLayout implements AudioSlidePlayer.Listener {
|
||||
|
||||
private static final String TAG = AudioView.class.getSimpleName();
|
||||
|
||||
private final @NonNull AnimatingToggle controlToggle;
|
||||
private final @NonNull ImageView playButton;
|
||||
private final @NonNull ImageView pauseButton;
|
||||
private final @NonNull SeekBar seekBar;
|
||||
private final @NonNull TextView timestamp;
|
||||
private final @NonNull TextView title;
|
||||
private final @NonNull View mask;
|
||||
|
||||
private @Nullable AudioSlidePlayer audioSlidePlayer;
|
||||
private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener;
|
||||
private int backwardsCounter;
|
||||
|
||||
public AudioView(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public AudioView(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public AudioView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
inflate(context, R.layout.audio_view, this);
|
||||
|
||||
this.controlToggle = (AnimatingToggle) findViewById(R.id.control_toggle);
|
||||
this.playButton = (ImageView) findViewById(R.id.play);
|
||||
this.pauseButton = (ImageView) findViewById(R.id.pause);
|
||||
this.seekBar = (SeekBar) findViewById(R.id.seek);
|
||||
this.timestamp = (TextView) findViewById(R.id.timestamp);
|
||||
this.title = (TextView) findViewById(R.id.title);
|
||||
this.mask = findViewById(R.id.interception_mask);
|
||||
|
||||
this.timestamp.setText("00:00");
|
||||
|
||||
this.playButton.setOnClickListener(new PlayClickedListener());
|
||||
this.pauseButton.setOnClickListener(new PauseClickedListener());
|
||||
this.seekBar.setOnSeekBarChangeListener(new SeekBarModifiedListener());
|
||||
|
||||
this.playButton.setImageDrawable(context.getDrawable(R.drawable.play_icon));
|
||||
this.pauseButton.setImageDrawable(context.getDrawable(R.drawable.pause_icon));
|
||||
this.playButton.setBackground(context.getDrawable(R.drawable.ic_circle_fill_white_48dp));
|
||||
this.pauseButton.setBackground(context.getDrawable(R.drawable.ic_circle_fill_white_48dp));
|
||||
|
||||
setTint(getContext().getResources().getColor(R.color.audio_icon));
|
||||
}
|
||||
|
||||
public void setAudio(final @NonNull AudioSlide audio, int duration)
|
||||
{
|
||||
controlToggle.displayQuick(playButton);
|
||||
seekBar.setEnabled(true);
|
||||
seekBar.setProgress(0);
|
||||
audioSlidePlayer = AudioSlidePlayer.createFor(getContext(), audio, this);
|
||||
timestamp.setText(DateUtils.getFormatedDuration(duration));
|
||||
|
||||
if(audio.asAttachment().isVoiceNote() || !audio.getFileName().isPresent()) {
|
||||
title.setVisibility(View.GONE);
|
||||
}
|
||||
else {
|
||||
title.setText(audio.getFileName().get());
|
||||
title.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnClickListener(OnClickListener listener) {
|
||||
super.setOnClickListener(listener);
|
||||
this.mask.setOnClickListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnLongClickListener(OnLongClickListener listener) {
|
||||
super.setOnLongClickListener(listener);
|
||||
this.mask.setOnLongClickListener(listener);
|
||||
this.playButton.setOnLongClickListener(listener);
|
||||
this.pauseButton.setOnLongClickListener(listener);
|
||||
}
|
||||
|
||||
public void togglePlay() {
|
||||
if (this.playButton.getVisibility() == View.VISIBLE) {
|
||||
playButton.performClick();
|
||||
} else {
|
||||
pauseButton.performClick();
|
||||
}
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
String desc;
|
||||
if (this.title.getVisibility() == View.VISIBLE) {
|
||||
desc = getContext().getString(R.string.audio);
|
||||
} else {
|
||||
desc = getContext().getString(R.string.voice_message);
|
||||
}
|
||||
desc += "\n" + this.timestamp.getText();
|
||||
if (title.getVisibility() == View.VISIBLE) {
|
||||
desc += "\n" + this.title.getText();
|
||||
}
|
||||
return desc;
|
||||
}
|
||||
|
||||
public void setDuration(int duration) {
|
||||
if (getProgress()==0)
|
||||
this.timestamp.setText(DateUtils.getFormatedDuration(duration));
|
||||
}
|
||||
|
||||
public void cleanup() {
|
||||
if (this.audioSlidePlayer != null && pauseButton.getVisibility() == View.VISIBLE) {
|
||||
this.audioSlidePlayer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceivedDuration(int millis) {
|
||||
this.timestamp.setText(DateUtils.getFormatedDuration(millis));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
if (this.pauseButton.getVisibility() != View.VISIBLE) {
|
||||
togglePlayToPause();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
if (this.playButton.getVisibility() != View.VISIBLE) {
|
||||
togglePauseToPlay();
|
||||
}
|
||||
|
||||
if (seekBar.getProgress() + 5 >= seekBar.getMax()) {
|
||||
backwardsCounter = 4;
|
||||
onProgress(audioSlidePlayer.getAudioSlide(), 0.0, -1);
|
||||
}
|
||||
}
|
||||
|
||||
public void disablePlayer(boolean disable) {
|
||||
this.mask.setVisibility(disable? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProgress(AudioSlide slide, double progress, long millis) {
|
||||
if (!audioSlidePlayer.getAudioSlide().equals(slide)) {
|
||||
return;
|
||||
}
|
||||
int seekProgress = (int) Math.floor(progress * this.seekBar.getMax());
|
||||
|
||||
if (seekProgress > seekBar.getProgress() || backwardsCounter > 3) {
|
||||
backwardsCounter = 0;
|
||||
this.seekBar.setProgress(seekProgress);
|
||||
if (millis != -1) {
|
||||
this.timestamp.setText(DateUtils.getFormatedDuration(millis));
|
||||
}
|
||||
} else {
|
||||
backwardsCounter++;
|
||||
}
|
||||
}
|
||||
|
||||
public void setTint(int foregroundTint) {
|
||||
this.playButton.setBackgroundTintList(ColorStateList.valueOf(foregroundTint));
|
||||
this.pauseButton.setBackgroundTintList(ColorStateList.valueOf(foregroundTint));
|
||||
|
||||
this.seekBar.getProgressDrawable().setColorFilter(foregroundTint, PorterDuff.Mode.SRC_IN);
|
||||
|
||||
this.seekBar.getThumb().setColorFilter(foregroundTint, PorterDuff.Mode.SRC_IN);
|
||||
}
|
||||
|
||||
public void getSeekBarGlobalVisibleRect(@NonNull Rect rect) {
|
||||
seekBar.getGlobalVisibleRect(rect);
|
||||
}
|
||||
|
||||
private double getProgress() {
|
||||
if (this.seekBar.getProgress() <= 0 || this.seekBar.getMax() <= 0) {
|
||||
return 0;
|
||||
} else {
|
||||
return (double)this.seekBar.getProgress() / (double)this.seekBar.getMax();
|
||||
}
|
||||
}
|
||||
|
||||
private void togglePlayToPause() {
|
||||
controlToggle.displayQuick(pauseButton);
|
||||
|
||||
AnimatedVectorDrawable playToPauseDrawable = (AnimatedVectorDrawable) getContext().getDrawable(R.drawable.play_to_pause_animation);
|
||||
pauseButton.setImageDrawable(playToPauseDrawable);
|
||||
playToPauseDrawable.start();
|
||||
}
|
||||
|
||||
private void togglePauseToPlay() {
|
||||
controlToggle.displayQuick(playButton);
|
||||
|
||||
AnimatedVectorDrawable pauseToPlayDrawable = (AnimatedVectorDrawable) getContext().getDrawable(R.drawable.pause_to_play_animation);
|
||||
playButton.setImageDrawable(pauseToPlayDrawable);
|
||||
pauseToPlayDrawable.start();
|
||||
}
|
||||
|
||||
private class PlayClickedListener implements View.OnClickListener {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
try {
|
||||
Log.w(TAG, "playbutton onClick");
|
||||
if (audioSlidePlayer != null) {
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
if (audioFocusChangeListener == null) {
|
||||
audioFocusChangeListener = focusChange -> {
|
||||
if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
|
||||
pauseButton.performClick();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
AudioAttributes playbackAttributes = new AudioAttributes.Builder()
|
||||
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
||||
.build();
|
||||
|
||||
AudioFocusRequest focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
|
||||
.setAudioAttributes(playbackAttributes)
|
||||
.setAcceptsDelayedFocusGain(false)
|
||||
.setWillPauseWhenDucked(false)
|
||||
.setOnAudioFocusChangeListener(audioFocusChangeListener)
|
||||
.build();
|
||||
|
||||
AudioManager audioManager = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
|
||||
audioManager.requestAudioFocus(focusRequest);
|
||||
}
|
||||
|
||||
togglePlayToPause();
|
||||
audioSlidePlayer.play(getProgress());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class PauseClickedListener implements View.OnClickListener {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Log.w(TAG, "pausebutton onClick");
|
||||
if (audioSlidePlayer != null) {
|
||||
togglePauseToPlay();
|
||||
audioSlidePlayer.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class SeekBarModifiedListener implements SeekBar.OnSeekBarChangeListener {
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {}
|
||||
|
||||
@Override
|
||||
public synchronized void onStartTrackingTouch(SeekBar seekBar) {
|
||||
if (audioSlidePlayer != null && pauseButton.getVisibility() == View.VISIBLE) {
|
||||
audioSlidePlayer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onStopTrackingTouch(SeekBar seekBar) {
|
||||
try {
|
||||
if (audioSlidePlayer != null && pauseButton.getVisibility() == View.VISIBLE) {
|
||||
audioSlidePlayer.play(getProgress());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,8 +67,10 @@ public class CallItemView extends FrameLayout {
|
||||
title.setText(R.string.canceled_call);
|
||||
} else if (callInfo.state instanceof CallState.Declined) {
|
||||
title.setText(R.string.declined_call);
|
||||
} else if (callInfo.hasVideo) {
|
||||
title.setText(isOutgoing? R.string.outgoing_video_call : R.string.incoming_video_call);
|
||||
} else {
|
||||
title.setText(isOutgoing? R.string.outgoing_call : R.string.incoming_call);
|
||||
title.setText(isOutgoing? R.string.outgoing_audio_call : R.string.incoming_audio_call);
|
||||
}
|
||||
|
||||
icon.setImageResource(callInfo.hasVideo? R.drawable.ic_videocam_white_24dp : R.drawable.baseline_call_24);
|
||||
|
||||
+20
-2
@@ -11,6 +11,9 @@ import android.widget.ImageView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class RemovableEditableMediaView extends FrameLayout {
|
||||
|
||||
private final @NonNull ImageView remove;
|
||||
@@ -19,6 +22,7 @@ public class RemovableEditableMediaView extends FrameLayout {
|
||||
private final int removeSize;
|
||||
|
||||
private @Nullable View current;
|
||||
private final List<OnClickListener> removeClickListeners = new ArrayList<>();
|
||||
|
||||
public RemovableEditableMediaView(Context context) {
|
||||
this(context, null);
|
||||
@@ -72,8 +76,22 @@ public class RemovableEditableMediaView extends FrameLayout {
|
||||
return current;
|
||||
}
|
||||
|
||||
public void setRemoveClickListener(View.OnClickListener listener) {
|
||||
this.remove.setOnClickListener(listener);
|
||||
public void addRemoveClickListener(View.OnClickListener listener) {
|
||||
removeClickListeners.add(listener);
|
||||
updateRemoveClickListener();
|
||||
}
|
||||
|
||||
public void removeRemoveClickListener(View.OnClickListener listener) {
|
||||
removeClickListeners.remove(listener);
|
||||
updateRemoveClickListener();
|
||||
}
|
||||
|
||||
private void updateRemoveClickListener() {
|
||||
this.remove.setOnClickListener(v -> {
|
||||
for (OnClickListener listener : removeClickListeners) {
|
||||
listener.onClick(v);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setEditClickListener(View.OnClickListener listener) {
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package org.thoughtcrime.securesms.components.audioplay;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class AudioPlaybackState {
|
||||
private final int msgId;
|
||||
private final @Nullable Uri audioUri;
|
||||
private final PlaybackStatus status;
|
||||
private final long currentPosition;
|
||||
private final long duration;
|
||||
|
||||
public enum PlaybackStatus {
|
||||
IDLE,
|
||||
LOADING,
|
||||
PLAYING,
|
||||
PAUSED,
|
||||
ERROR
|
||||
}
|
||||
|
||||
public AudioPlaybackState(int msgId,
|
||||
@Nullable Uri audioUri,
|
||||
PlaybackStatus status,
|
||||
long currentPosition,
|
||||
long duration) {
|
||||
this.msgId = msgId;
|
||||
this.audioUri = audioUri;
|
||||
this.status = status;
|
||||
this.currentPosition = currentPosition;
|
||||
this.duration = duration;
|
||||
}
|
||||
|
||||
public static AudioPlaybackState idle() {
|
||||
return new AudioPlaybackState(0, null, PlaybackStatus.IDLE, 0, 0);
|
||||
}
|
||||
|
||||
public int getMsgId() {
|
||||
return msgId;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Uri getAudioUri() {
|
||||
return audioUri;
|
||||
}
|
||||
|
||||
public PlaybackStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public long getCurrentPosition() {
|
||||
return currentPosition;
|
||||
}
|
||||
|
||||
public long getDuration() {
|
||||
return duration;
|
||||
}
|
||||
}
|
||||
+243
@@ -0,0 +1,243 @@
|
||||
package org.thoughtcrime.securesms.components.audioplay;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
|
||||
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.Player;
|
||||
import androidx.media3.session.MediaController;
|
||||
|
||||
|
||||
public class AudioPlaybackViewModel extends ViewModel {
|
||||
private static final String TAG = AudioPlaybackViewModel.class.getSimpleName();
|
||||
|
||||
private static final int NON_MESSAGE_AUDIO_MSG_ID = 0; // Audios not attached to a message doesn't have message id.
|
||||
|
||||
private final MutableLiveData<AudioPlaybackState> playbackState;
|
||||
private @Nullable MediaController mediaController;
|
||||
private final Handler handler;
|
||||
private boolean isUserSeeking = false;
|
||||
|
||||
public AudioPlaybackViewModel() {
|
||||
playbackState = new MutableLiveData<>(AudioPlaybackState.idle());
|
||||
handler = new Handler(Looper.getMainLooper());
|
||||
}
|
||||
|
||||
public LiveData<AudioPlaybackState> getPlaybackState() {
|
||||
return playbackState;
|
||||
}
|
||||
|
||||
public void setMediaController(@Nullable MediaController controller) {
|
||||
this.mediaController = controller;
|
||||
if (mediaController != null && mediaController.isPlaying()) {
|
||||
startUpdateProgress();
|
||||
}
|
||||
updateCurrentState(true);
|
||||
setupPlayerListener();
|
||||
}
|
||||
|
||||
// Public methods
|
||||
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);
|
||||
|
||||
MediaItem mediaItem = new MediaItem.Builder()
|
||||
.setMediaId(String.valueOf(msgId))
|
||||
.setUri(audioUri)
|
||||
.build();
|
||||
mediaController.setMediaItem(mediaItem);
|
||||
mediaController.prepare();
|
||||
}
|
||||
|
||||
play(msgId, audioUri);
|
||||
}
|
||||
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));
|
||||
}
|
||||
|
||||
public void pause(int msgId, Uri audioUri) {
|
||||
if (mediaController != null && isSameAudio(msgId, audioUri)) {
|
||||
mediaController.pause();
|
||||
}
|
||||
}
|
||||
|
||||
public void play(int msgId, Uri audioUri) {
|
||||
if (mediaController != null && isSameAudio(msgId, audioUri)) {
|
||||
mediaController.play();
|
||||
}
|
||||
}
|
||||
|
||||
public void seekTo(long position, int msgId, Uri audioUri) {
|
||||
if (mediaController != null && isSameAudio(msgId, audioUri)) {
|
||||
mediaController.seekTo(position);
|
||||
}
|
||||
}
|
||||
|
||||
public void stop(int msgId, Uri audioUri) {
|
||||
if (mediaController != null && isSameAudio(msgId, audioUri)) {
|
||||
mediaController.stop();
|
||||
stopUpdateProgress();
|
||||
playbackState.setValue(AudioPlaybackState.idle());
|
||||
}
|
||||
}
|
||||
|
||||
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 && currentState != null) {
|
||||
for (int msgId : msgIds) {
|
||||
if (msgId == currentState.getMsgId()) {
|
||||
mediaController.stop();
|
||||
stopUpdateProgress();
|
||||
playbackState.setValue(AudioPlaybackState.idle());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setUserSeeking(boolean isUserSeeking) {
|
||||
this.isUserSeeking = isUserSeeking;
|
||||
}
|
||||
|
||||
// Private methods
|
||||
private void setupPlayerListener() {
|
||||
if (mediaController == null) return;
|
||||
|
||||
mediaController.addListener(new Player.Listener() {
|
||||
@Override
|
||||
public void onEvents(Player player, Player.Events events) {
|
||||
if (events.containsAny(Player.EVENT_IS_PLAYING_CHANGED)) {
|
||||
if (player.isPlaying()) {
|
||||
startUpdateProgress();
|
||||
} else {
|
||||
stopUpdateProgress();
|
||||
}
|
||||
updateCurrentState(false);
|
||||
}
|
||||
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
|
||||
mediaController.setPlayWhenReady(false);
|
||||
}
|
||||
}
|
||||
if (events.containsAny(Player.EVENT_PLAYER_ERROR)) {
|
||||
updateCurrentAudioState(AudioPlaybackState.PlaybackStatus.ERROR, 0, 0);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void updateCurrentState(boolean queryPlaying) {
|
||||
if (mediaController == null) return;
|
||||
|
||||
AudioPlaybackState.PlaybackStatus status;
|
||||
if (mediaController.isPlaying()) {
|
||||
status = AudioPlaybackState.PlaybackStatus.PLAYING;
|
||||
} else if (mediaController.getPlaybackState() == Player.STATE_READY
|
||||
|| mediaController.getPlaybackState() == Player.STATE_ENDED) {
|
||||
status = AudioPlaybackState.PlaybackStatus.PAUSED;
|
||||
} else {
|
||||
status = AudioPlaybackState.PlaybackStatus.IDLE;
|
||||
}
|
||||
|
||||
Uri currentUri = null;
|
||||
int currentMsgId = 0;
|
||||
if (playbackState.getValue() != null) {
|
||||
currentMsgId = playbackState.getValue().getMsgId();
|
||||
currentUri = playbackState.getValue().getAudioUri();
|
||||
}
|
||||
if (queryPlaying || playbackState.getValue() == null) {
|
||||
MediaItem item = mediaController.getCurrentMediaItem();
|
||||
if (item != null) {
|
||||
try {
|
||||
currentMsgId = Integer.parseInt(item.mediaId);
|
||||
} catch (NumberFormatException e) {
|
||||
Log.w(TAG, "Invalid integer", e);
|
||||
}
|
||||
if (item.localConfiguration != null) {
|
||||
currentUri = item.localConfiguration.uri;
|
||||
}
|
||||
}
|
||||
}
|
||||
updateState(
|
||||
currentMsgId,
|
||||
currentUri,
|
||||
status,
|
||||
mediaController.getCurrentPosition(),
|
||||
mediaController.getDuration());
|
||||
}
|
||||
|
||||
private void updateState(int msgId,
|
||||
Uri audioUri,
|
||||
AudioPlaybackState.PlaybackStatus status,
|
||||
long position,
|
||||
long duration) {
|
||||
playbackState.setValue(new AudioPlaybackState(
|
||||
msgId, audioUri, status, position, duration
|
||||
));
|
||||
}
|
||||
|
||||
private void updateCurrentAudioState(AudioPlaybackState.PlaybackStatus status,
|
||||
long position,
|
||||
long duration) {
|
||||
AudioPlaybackState current = playbackState.getValue();
|
||||
|
||||
if (current != null) {
|
||||
updateState(current.getMsgId(), current.getAudioUri(), status, position, duration);
|
||||
}
|
||||
}
|
||||
|
||||
// Progress tracking
|
||||
private final Runnable progressRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (mediaController != null && mediaController.isPlaying() && !isUserSeeking) {
|
||||
updateCurrentAudioState(AudioPlaybackState.PlaybackStatus.PLAYING,
|
||||
mediaController.getCurrentPosition(),
|
||||
mediaController.getDuration());
|
||||
handler.postDelayed(this, 100);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private void startUpdateProgress() {
|
||||
stopUpdateProgress();
|
||||
handler.post(progressRunnable);
|
||||
}
|
||||
|
||||
private void stopUpdateProgress() {
|
||||
handler.removeCallbacks(progressRunnable);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCleared() {
|
||||
stopUpdateProgress();
|
||||
super.onCleared();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
package org.thoughtcrime.securesms.components.audioplay;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.vectordrawable.graphics.drawable.Animatable2Compat;
|
||||
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.mms.AudioSlide;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
|
||||
|
||||
public class AudioView extends FrameLayout {
|
||||
|
||||
private static final String TAG = AudioView.class.getSimpleName();
|
||||
|
||||
private final @NonNull ImageView playPauseButton;
|
||||
private final AnimatedVectorDrawableCompat playToPauseDrawable;
|
||||
private final AnimatedVectorDrawableCompat pauseToPlayDrawable;
|
||||
private final Drawable playDrawable;
|
||||
private final Drawable pauseDrawable;
|
||||
private final Animatable2Compat.AnimationCallback animationCallback;
|
||||
private final @NonNull SeekBar seekBar;
|
||||
private final @NonNull TextView timestamp;
|
||||
private final @NonNull TextView title;
|
||||
private final @NonNull View mask;
|
||||
private OnActionListener listener;
|
||||
|
||||
private int msgId;
|
||||
private Uri audioUri;
|
||||
private AudioPlaybackViewModel viewModel;
|
||||
private final Observer<AudioPlaybackState> stateObserver = this::onPlaybackStateChanged;
|
||||
private boolean isPlaying;
|
||||
|
||||
public AudioView(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public AudioView(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public AudioView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
inflate(context, R.layout.audio_view, this);
|
||||
|
||||
this.playPauseButton = findViewById(R.id.play_pause);
|
||||
this.seekBar = findViewById(R.id.seek);
|
||||
this.timestamp = findViewById(R.id.timestamp);
|
||||
this.title = findViewById(R.id.title);
|
||||
this.mask = findViewById(R.id.interception_mask);
|
||||
|
||||
this.timestamp.setText("00:00");
|
||||
|
||||
// Load drawables once
|
||||
this.playToPauseDrawable = AnimatedVectorDrawableCompat.create(
|
||||
getContext(), R.drawable.play_to_pause_animation);
|
||||
this.pauseToPlayDrawable = AnimatedVectorDrawableCompat.create(
|
||||
getContext(), R.drawable.pause_to_play_animation);
|
||||
this.playDrawable = AppCompatResources.getDrawable(getContext(), R.drawable.play_icon);
|
||||
this.pauseDrawable = AppCompatResources.getDrawable(getContext(), R.drawable.pause_icon);
|
||||
|
||||
this.animationCallback = new Animatable2Compat.AnimationCallback() {
|
||||
@Override
|
||||
public void onAnimationEnd(Drawable drawable) {
|
||||
Drawable endState = isPlaying ? pauseDrawable : playDrawable;
|
||||
playPauseButton.setImageDrawable(endState);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
setupControls();
|
||||
}
|
||||
|
||||
private void setupControls() {
|
||||
playPauseButton.setOnClickListener(v -> {
|
||||
Log.w(TAG, "playPauseButton onClick");
|
||||
|
||||
if (viewModel == null || audioUri == null) return;
|
||||
|
||||
AudioPlaybackState state = viewModel.getPlaybackState().getValue();
|
||||
|
||||
if (state != null && msgId == state.getMsgId() && audioUri.equals(state.getAudioUri())) {
|
||||
// Same audio
|
||||
if (state.getStatus() == AudioPlaybackState.PlaybackStatus.PLAYING) {
|
||||
viewModel.pause(msgId, audioUri);
|
||||
} else {
|
||||
viewModel.play(msgId, audioUri);
|
||||
}
|
||||
} else {
|
||||
// Different audio
|
||||
// Note: they can be the same *physical* file, but in different messages
|
||||
viewModel.loadAudioAndPlay(msgId, audioUri);
|
||||
}
|
||||
|
||||
if (listener != null) {
|
||||
listener.onPlayPauseButtonClicked(v);
|
||||
}
|
||||
});
|
||||
|
||||
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
if (fromUser) {
|
||||
timestamp.setText(DateUtils.getFormatedDuration(progress));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
viewModel.setUserSeeking(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||
viewModel.setUserSeeking(false);
|
||||
viewModel.seekTo(seekBar.getProgress(), msgId, audioUri);
|
||||
}
|
||||
});
|
||||
|
||||
if (playToPauseDrawable != null) {
|
||||
playToPauseDrawable.registerAnimationCallback(animationCallback);
|
||||
}
|
||||
if (pauseToPlayDrawable != null) {
|
||||
pauseToPlayDrawable.registerAnimationCallback(animationCallback);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
if (viewModel != null) {
|
||||
viewModel.getPlaybackState().removeObserver(stateObserver);
|
||||
}
|
||||
if (playToPauseDrawable != null) {
|
||||
playToPauseDrawable.clearAnimationCallbacks();
|
||||
}
|
||||
if (pauseToPlayDrawable != null) {
|
||||
pauseToPlayDrawable.clearAnimationCallbacks();
|
||||
}
|
||||
super.onDetachedFromWindow();
|
||||
}
|
||||
|
||||
public void setPlaybackViewModel(AudioPlaybackViewModel viewModel) {
|
||||
if (this.viewModel != null) {
|
||||
this.viewModel.getPlaybackState().removeObserver(stateObserver);
|
||||
}
|
||||
|
||||
// ViewModel is used directly for simplicity, since there is no reuse yet
|
||||
this.viewModel = viewModel;
|
||||
|
||||
if (viewModel != null) {
|
||||
viewModel.getPlaybackState().observeForever(stateObserver);
|
||||
}
|
||||
}
|
||||
|
||||
public void setAudio(final @NonNull AudioSlide audio, int duration)
|
||||
{
|
||||
msgId = audio.getDcMsgId();
|
||||
audioUri = audio.getUri();
|
||||
playPauseButton.setImageDrawable(playDrawable);
|
||||
|
||||
seekBar.setEnabled(true);
|
||||
seekBar.setProgress(0);
|
||||
timestamp.setText(DateUtils.getFormatedDuration(duration));
|
||||
|
||||
if(audio.asAttachment().isVoiceNote() || !audio.getFileName().isPresent()) {
|
||||
title.setVisibility(View.GONE);
|
||||
}
|
||||
else {
|
||||
title.setText(audio.getFileName().get());
|
||||
title.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnClickListener(OnClickListener listener) {
|
||||
super.setOnClickListener(listener);
|
||||
this.mask.setOnClickListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnLongClickListener(OnLongClickListener listener) {
|
||||
super.setOnLongClickListener(listener);
|
||||
this.mask.setOnLongClickListener(listener);
|
||||
this.playPauseButton.setOnLongClickListener(listener);
|
||||
}
|
||||
|
||||
public int getMsgId() {
|
||||
return msgId;
|
||||
}
|
||||
|
||||
public Uri getAudioUri() {
|
||||
return audioUri;
|
||||
}
|
||||
|
||||
public interface OnActionListener {
|
||||
void onPlayPauseButtonClicked(View view);
|
||||
}
|
||||
|
||||
public void setOnActionListener(OnActionListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public void togglePlay() {
|
||||
playPauseButton.performClick();
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
String desc;
|
||||
if (this.title.getVisibility() == View.VISIBLE) {
|
||||
desc = getContext().getString(R.string.audio);
|
||||
} else {
|
||||
desc = getContext().getString(R.string.voice_message);
|
||||
}
|
||||
desc += "\n" + this.timestamp.getText();
|
||||
if (title.getVisibility() == View.VISIBLE) {
|
||||
desc += "\n" + this.title.getText();
|
||||
}
|
||||
return desc;
|
||||
}
|
||||
|
||||
private void updateProgress(AudioPlaybackState state) {
|
||||
int duration = (int) state.getDuration();
|
||||
int position = (int) state.getCurrentPosition();
|
||||
|
||||
if (duration > 0) {
|
||||
seekBar.setMax(duration);
|
||||
seekBar.setProgress(position);
|
||||
timestamp.setText(DateUtils.getFormatedDuration(position));
|
||||
}
|
||||
}
|
||||
|
||||
public void disablePlayer(boolean disable) {
|
||||
this.mask.setVisibility(disable? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
public void getSeekBarGlobalVisibleRect(@NonNull Rect rect) {
|
||||
seekBar.getGlobalVisibleRect(rect);
|
||||
}
|
||||
|
||||
private void togglePlayPause(boolean expectedPlaying) {
|
||||
isPlaying = expectedPlaying;
|
||||
Drawable expectedDrawable = expectedPlaying ? pauseDrawable : playDrawable;
|
||||
|
||||
boolean isAnimating = false;
|
||||
Drawable currentDrawable = playPauseButton.getDrawable();
|
||||
if (currentDrawable instanceof AnimatedVectorDrawableCompat) {
|
||||
isAnimating = ((AnimatedVectorDrawableCompat) currentDrawable).isRunning();
|
||||
}
|
||||
if (!isAnimating && playPauseButton.getDrawable() != expectedDrawable) {
|
||||
AnimatedVectorDrawableCompat animDrawable = expectedPlaying ? playToPauseDrawable : pauseToPlayDrawable;
|
||||
String contentDescription = getContext().getString(
|
||||
expectedPlaying ? R.string.menu_pause : R.string.menu_play);
|
||||
|
||||
if (animDrawable != null) {
|
||||
playPauseButton.setImageDrawable(animDrawable);
|
||||
playPauseButton.setContentDescription(contentDescription);
|
||||
|
||||
animDrawable.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onPlaybackStateChanged(AudioPlaybackState state) {
|
||||
if (audioUri == null || state == null) return;
|
||||
|
||||
// Check if this state is about this message
|
||||
boolean isThisMessage = msgId == state.getMsgId() && audioUri.equals(state.getAudioUri());
|
||||
|
||||
if (isThisMessage) {
|
||||
updateUIForPlaybackState(state);
|
||||
} else {
|
||||
togglePlayPause(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateUIForPlaybackState(AudioPlaybackState state) {
|
||||
switch (state.getStatus()) {
|
||||
case PLAYING:
|
||||
togglePlayPause(true);
|
||||
updateProgress(state);
|
||||
break;
|
||||
|
||||
case PAUSED:
|
||||
togglePlayPause(false);
|
||||
updateProgress(state);
|
||||
break;
|
||||
|
||||
case LOADING:
|
||||
case ERROR:
|
||||
// No special handling yet
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -210,8 +210,6 @@ public class DcHelper {
|
||||
dcContext.setStockTranslation(178, context.getString(R.string.member_x_removed));
|
||||
dcContext.setStockTranslation(190, context.getString(R.string.secure_join_wait));
|
||||
dcContext.setStockTranslation(193, context.getString(R.string.donate_device_msg));
|
||||
dcContext.setStockTranslation(194, context.getString(R.string.outgoing_call));
|
||||
dcContext.setStockTranslation(195, context.getString(R.string.incoming_call));
|
||||
dcContext.setStockTranslation(196, context.getString(R.string.declined_call));
|
||||
dcContext.setStockTranslation(197, context.getString(R.string.canceled_call));
|
||||
dcContext.setStockTranslation(198, context.getString(R.string.missed_call));
|
||||
@@ -223,6 +221,10 @@ public class DcHelper {
|
||||
dcContext.setStockTranslation(220, context.getString(R.string.proxy_enabled));
|
||||
dcContext.setStockTranslation(221, context.getString(R.string.proxy_enabled_hint));
|
||||
dcContext.setStockTranslation(230, context.getString(R.string.chat_unencrypted_explanation));
|
||||
dcContext.setStockTranslation(232, context.getString(R.string.outgoing_audio_call));
|
||||
dcContext.setStockTranslation(233, context.getString(R.string.outgoing_video_call));
|
||||
dcContext.setStockTranslation(234, context.getString(R.string.incoming_audio_call));
|
||||
dcContext.setStockTranslation(235, context.getString(R.string.incoming_video_call));
|
||||
}
|
||||
|
||||
public static File getImexDir() {
|
||||
|
||||
@@ -49,13 +49,13 @@ import org.thoughtcrime.securesms.WebxdcActivity;
|
||||
import org.thoughtcrime.securesms.WebxdcStoreActivity;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.UriAttachment;
|
||||
import org.thoughtcrime.securesms.audio.AudioSlidePlayer;
|
||||
import org.thoughtcrime.securesms.components.AudioView;
|
||||
import org.thoughtcrime.securesms.components.DocumentView;
|
||||
import org.thoughtcrime.securesms.components.RemovableEditableMediaView;
|
||||
import org.thoughtcrime.securesms.components.ThumbnailView;
|
||||
import org.thoughtcrime.securesms.components.VcardView;
|
||||
import org.thoughtcrime.securesms.components.WebxdcView;
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioPlaybackViewModel;
|
||||
import org.thoughtcrime.securesms.components.audioplay.AudioView;
|
||||
import org.thoughtcrime.securesms.connect.DcHelper;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.geolocation.DcLocationManager;
|
||||
@@ -121,7 +121,7 @@ public class AttachmentManager {
|
||||
//this.mapView = ViewUtil.findById(root, R.id.attachment_location);
|
||||
this.removableMediaView = ViewUtil.findById(root, R.id.removable_media_view);
|
||||
|
||||
removableMediaView.setRemoveClickListener(new RemoveButtonListener());
|
||||
removableMediaView.addRemoveClickListener(new RemoveButtonListener());
|
||||
removableMediaView.setEditClickListener(new EditButtonListener());
|
||||
thumbnail.setOnClickListener(new ThumbnailClickListener());
|
||||
}
|
||||
@@ -152,8 +152,6 @@ public class AttachmentManager {
|
||||
|
||||
markGarbage(getSlideUri());
|
||||
slide = Optional.absent();
|
||||
|
||||
audioView.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,7 +233,8 @@ public class AttachmentManager {
|
||||
@NonNull final MediaType mediaType,
|
||||
final int width,
|
||||
final int height,
|
||||
final int chatId)
|
||||
final int chatId,
|
||||
AudioPlaybackViewModel playbackViewModel)
|
||||
{
|
||||
inflateStub();
|
||||
|
||||
@@ -285,26 +284,12 @@ public class AttachmentManager {
|
||||
setAttachmentPresent(true);
|
||||
|
||||
if (slide.hasAudio()) {
|
||||
class SetDurationListener implements AudioSlidePlayer.Listener {
|
||||
@Override
|
||||
public void onStart() {}
|
||||
|
||||
@Override
|
||||
public void onStop() {}
|
||||
|
||||
@Override
|
||||
public void onProgress(AudioSlide slide, double progress, long millis) {}
|
||||
|
||||
@Override
|
||||
public void onReceivedDuration(int millis) {
|
||||
((AudioView) removableMediaView.getCurrent()).setDuration(millis);
|
||||
}
|
||||
}
|
||||
AudioSlidePlayer audioSlidePlayer = AudioSlidePlayer.createFor(context, (AudioSlide) slide, new SetDurationListener());
|
||||
audioSlidePlayer.requestDuration();
|
||||
|
||||
audioView.setPlaybackViewModel(playbackViewModel);
|
||||
audioView.setAudio((AudioSlide) slide, 0);
|
||||
removableMediaView.display(audioView, false);
|
||||
removableMediaView.addRemoveClickListener(v -> {
|
||||
playbackViewModel.stop(audioView.getMsgId(), audioView.getAudioUri());
|
||||
});
|
||||
result.set(true);
|
||||
} else if (slide.isVcard()) {
|
||||
vcardView.setVcard(glideRequests, (VcardSlide)slide, DcHelper.getRpc(context));
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
package org.thoughtcrime.securesms.service;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.OptIn;
|
||||
import androidx.media3.common.AudioAttributes;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.exoplayer.ExoPlayer;
|
||||
import androidx.media3.session.MediaSession;
|
||||
import androidx.media3.session.MediaSessionService;
|
||||
import androidx.media3.session.SessionCommand;
|
||||
import androidx.media3.session.SessionCommands;
|
||||
import androidx.media3.session.SessionResult;
|
||||
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import org.thoughtcrime.securesms.ConversationListActivity;
|
||||
|
||||
public class AudioPlaybackService extends MediaSessionService {
|
||||
|
||||
private static final String TAG = AudioPlaybackService.class.getSimpleName();
|
||||
|
||||
private ExoPlayer player;
|
||||
private MediaSession session;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
AudioAttributes audioAttributes = new AudioAttributes.Builder()
|
||||
.setUsage(C.USAGE_MEDIA) // USAGE_VOICE_COMMUNICATION is for VoIP calls
|
||||
.setContentType(C.AUDIO_CONTENT_TYPE_SPEECH)
|
||||
.build();
|
||||
|
||||
player = new ExoPlayer.Builder(this)
|
||||
.setAudioAttributes(audioAttributes, true)
|
||||
.setHandleAudioBecomingNoisy(true)
|
||||
.build();
|
||||
|
||||
// This is for click on the notification to go back to app
|
||||
Intent intent = new Intent(this, ConversationListActivity.class);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
PendingIntent initialIntent = PendingIntent.getActivity(
|
||||
this, 0, intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
|
||||
);
|
||||
|
||||
session = new MediaSession.Builder(this, player)
|
||||
.setSessionActivity(initialIntent)
|
||||
.setCallback(new MediaSession.Callback() {
|
||||
|
||||
@OptIn(markerClass = UnstableApi.class)
|
||||
@Override
|
||||
public MediaSession.ConnectionResult onConnect(
|
||||
MediaSession session,
|
||||
MediaSession.ControllerInfo controller
|
||||
) {
|
||||
SessionCommands sessionCommands = MediaSession
|
||||
.ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
|
||||
.add(new SessionCommand("UPDATE_ACTIVITY_CONTEXT", new Bundle()))
|
||||
.build();
|
||||
|
||||
return new MediaSession.ConnectionResult.AcceptedResultBuilder(session)
|
||||
.setAvailableSessionCommands(sessionCommands)
|
||||
.build();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ListenableFuture<SessionResult> onCustomCommand(
|
||||
MediaSession session,
|
||||
MediaSession.ControllerInfo controller,
|
||||
SessionCommand customCommand,
|
||||
Bundle args
|
||||
) {
|
||||
if ("UPDATE_ACTIVITY_CONTEXT".equals(customCommand.customAction)) {
|
||||
updateSessionActivity(args);
|
||||
}
|
||||
return Futures.immediateFuture(
|
||||
new SessionResult(SessionResult.RESULT_SUCCESS));
|
||||
}
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
@OptIn(markerClass = UnstableApi.class)
|
||||
private void updateSessionActivity(Bundle args) {
|
||||
try {
|
||||
// Put all the original extras back into the intent
|
||||
if (args != null && !args.isEmpty()) {
|
||||
String activityClassName = args.getString("activity_class");
|
||||
args.remove("activity_class");
|
||||
|
||||
if (activityClassName != null) {
|
||||
Class<?> activityClass = Class.forName(activityClassName);
|
||||
Intent intent = new Intent(this, activityClass);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
intent.putExtras(args);
|
||||
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
|
||||
);
|
||||
|
||||
session.setSessionActivity(pendingIntent);
|
||||
}
|
||||
}
|
||||
} catch (ClassNotFoundException e) {
|
||||
Log.e(TAG, "Activity class not found", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public MediaSession onGetSession(MediaSession.ControllerInfo controllerInfo) {
|
||||
return session;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
if (session != null) {
|
||||
session.release();
|
||||
session = null;
|
||||
}
|
||||
if (player != null) {
|
||||
player.release();
|
||||
player = null;
|
||||
}
|
||||
super.onDestroy();
|
||||
}
|
||||
}
|
||||
@@ -323,7 +323,15 @@ public class ViewUtil {
|
||||
* @param view The view to apply insets to
|
||||
*/
|
||||
public static void applyWindowInsetsAsMargin(@NonNull View view) {
|
||||
applyWindowInsetsAsMargin(view, true, true, true, true);
|
||||
applyWindowInsetsAsMargin(view, true, true, true, true, false);
|
||||
}
|
||||
|
||||
public static void applyWindowInsetsAsMargin(@NonNull View view, boolean left, boolean top, boolean right, boolean bottom) {
|
||||
applyWindowInsetsAsMargin(view, left, top, right, bottom, false);
|
||||
}
|
||||
|
||||
public static void forceApplyWindowInsetsAsMargin(@NonNull View view, boolean left, boolean top, boolean right, boolean bottom) {
|
||||
applyWindowInsetsAsMargin(view, left, top, right, bottom, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -337,8 +345,9 @@ public class ViewUtil {
|
||||
* @param top Whether to apply top inset
|
||||
* @param right Whether to apply right inset
|
||||
* @param bottom Whether to apply bottom inset
|
||||
* @param forceDispatch Whether to force application of insets
|
||||
*/
|
||||
public static void applyWindowInsetsAsMargin(@NonNull View view, boolean left, boolean top, boolean right, boolean bottom) {
|
||||
public static void applyWindowInsetsAsMargin(@NonNull View view, boolean left, boolean top, boolean right, boolean bottom, boolean forceDispatch) {
|
||||
// Only enable on API 30+ where WindowInsets APIs work correctly
|
||||
if (!isEdgeToEdgeSupported()) return;
|
||||
|
||||
@@ -371,10 +380,10 @@ public class ViewUtil {
|
||||
ViewGroup.LayoutParams layoutParams = v.getLayoutParams();
|
||||
if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
|
||||
ViewGroup.MarginLayoutParams marginParams = (ViewGroup.MarginLayoutParams) layoutParams;
|
||||
marginParams.leftMargin = baseMarginLeft + insets.left;
|
||||
marginParams.topMargin = baseMarginTop + insets.top;
|
||||
marginParams.rightMargin = baseMarginRight + insets.right;
|
||||
marginParams.bottomMargin = baseMarginBottom + insets.bottom;
|
||||
marginParams.leftMargin = left ? baseMarginLeft + insets.left : baseMarginLeft;
|
||||
marginParams.topMargin = top ? baseMarginTop + insets.top : baseMarginTop;
|
||||
marginParams.rightMargin = right ? baseMarginRight + insets.right : baseMarginRight;
|
||||
marginParams.bottomMargin = bottom ? baseMarginBottom + insets.bottom : baseMarginBottom;
|
||||
v.setLayoutParams(marginParams);
|
||||
}
|
||||
|
||||
@@ -383,7 +392,14 @@ public class ViewUtil {
|
||||
|
||||
// Request the initial insets to be dispatched if the view is attached
|
||||
if (view.isAttachedToWindow()) {
|
||||
ViewCompat.requestApplyInsets(view);
|
||||
if (forceDispatch) {
|
||||
WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(view);
|
||||
if (insets != null) {
|
||||
ViewCompat.dispatchApplyWindowInsets(view, insets);
|
||||
}
|
||||
} else {
|
||||
ViewCompat.requestApplyInsets(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -395,7 +411,7 @@ public class ViewUtil {
|
||||
* @param view The view to apply insets to
|
||||
*/
|
||||
public static void applyWindowInsets(@NonNull View view) {
|
||||
applyWindowInsets(view, true, true, true, true, false);
|
||||
applyWindowInsets(view, true, true, true, true, false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -410,7 +426,20 @@ public class ViewUtil {
|
||||
* @param bottom Whether to apply bottom inset
|
||||
*/
|
||||
public static void applyWindowInsets(@NonNull View view, boolean left, boolean top, boolean right, boolean bottom) {
|
||||
applyWindowInsets(view, left, top, right, bottom, false);
|
||||
applyWindowInsets(view, left, top, right, bottom, false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Force applying window insets to a view by adding padding to avoid drawing elements behind system bars.
|
||||
*
|
||||
* @param view The view to apply insets to
|
||||
* @param left Whether to apply left inset
|
||||
* @param top Whether to apply top inset
|
||||
* @param right Whether to apply right inset
|
||||
* @param bottom Whether to apply bottom inset
|
||||
*/
|
||||
public static void forceApplyWindowInsets(@NonNull View view, boolean left, boolean top, boolean right, boolean bottom) {
|
||||
applyWindowInsets(view, left, top, right, bottom, false, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -425,8 +454,9 @@ public class ViewUtil {
|
||||
* @param right Whether to apply right inset
|
||||
* @param bottom Whether to apply bottom inset
|
||||
* @param consumeImeInsets Whether to consume IME insets so they don't propagate to child views
|
||||
* @param forceDispatch Force application of Insets, regardless if system think it shall dispatch
|
||||
*/
|
||||
public static void applyWindowInsets(@NonNull View view, boolean left, boolean top, boolean right, boolean bottom, boolean consumeImeInsets) {
|
||||
public static void applyWindowInsets(@NonNull View view, boolean left, boolean top, boolean right, boolean bottom, boolean consumeImeInsets, boolean forceDispatch) {
|
||||
// Only enable on API 30+ where WindowInsets APIs work correctly
|
||||
if (!isEdgeToEdgeSupported()) return;
|
||||
|
||||
@@ -469,7 +499,14 @@ public class ViewUtil {
|
||||
|
||||
// Request the initial insets to be dispatched if the view is attached
|
||||
if (view.isAttachedToWindow()) {
|
||||
ViewCompat.requestApplyInsets(view);
|
||||
if (forceDispatch) {
|
||||
WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(view);
|
||||
if (insets != null) {
|
||||
ViewCompat.dispatchApplyWindowInsets(view, insets);
|
||||
}
|
||||
} else {
|
||||
ViewCompat.requestApplyInsets(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context="org.thoughtcrime.securesms.components.AudioView">
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<LinearLayout android:id="@+id/audio_widget_container"
|
||||
android:orientation="vertical"
|
||||
@@ -12,39 +10,19 @@
|
||||
android:layout_height="fill_parent"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<org.thoughtcrime.securesms.components.AnimatingToggle
|
||||
android:id="@+id/control_toggle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center">
|
||||
|
||||
<ImageView android:id="@+id/play"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:gravity="center_vertical"
|
||||
android:clickable="true"
|
||||
android:visibility="gone"
|
||||
android:background="@drawable/circle_touch_highlight_background"
|
||||
android:src="@drawable/ic_play_circle_fill_white_48dp"
|
||||
android:scaleType="centerInside"
|
||||
tools:visibility="gone"
|
||||
android:contentDescription="@string/menu_play"/>
|
||||
|
||||
<ImageView android:id="@+id/pause"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:gravity="center_vertical"
|
||||
android:clickable="true"
|
||||
android:visibility="gone"
|
||||
android:background="@drawable/circle_touch_highlight_background"
|
||||
android:src="@drawable/ic_pause_circle_fill_white_48dp"
|
||||
android:scaleType="centerInside"
|
||||
android:contentDescription="@string/menu_pause"/>
|
||||
|
||||
</org.thoughtcrime.securesms.components.AnimatingToggle>
|
||||
<ImageView android:id="@+id/play_pause"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:focusable="true"
|
||||
android:gravity="center_vertical"
|
||||
android:clickable="true"
|
||||
android:visibility="visible"
|
||||
android:background="@drawable/ic_circle_fill_white_48dp"
|
||||
android:backgroundTint="@color/audio_icon"
|
||||
android:src="@drawable/play_icon"
|
||||
android:scaleType="centerInside"
|
||||
android:contentDescription="@string/menu_play"/>
|
||||
|
||||
<LinearLayout android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
@@ -54,7 +32,11 @@
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="2"
|
||||
android:layout_gravity="center_vertical"/>
|
||||
android:layout_gravity="center_vertical"
|
||||
android:progressTint="@color/audio_icon"
|
||||
android:progressTintMode="src_in"
|
||||
android:thumbTint="@color/audio_icon"
|
||||
android:thumbTintMode="src_in"/>
|
||||
|
||||
<LinearLayout android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
app:minHeight="100dp"
|
||||
app:maxHeight="300dp" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.AudioView
|
||||
<org.thoughtcrime.securesms.components.audioplay.AudioView
|
||||
android:id="@+id/attachment_audio"
|
||||
android:layout_width="230dp"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
/>
|
||||
|
||||
<View
|
||||
android:id="@+id/bottom_divider"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="2dp"
|
||||
android:layout_gravity="bottom"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.thoughtcrime.securesms.components.AudioView
|
||||
<org.thoughtcrime.securesms.components.audioplay.AudioView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="210dp"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.thoughtcrime.securesms.components.AudioView
|
||||
<org.thoughtcrime.securesms.components.audioplay.AudioView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="210dp"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
android:layout_weight="1"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<org.thoughtcrime.securesms.components.AudioView
|
||||
<org.thoughtcrime.securesms.components.audioplay.AudioView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/audio_view"
|
||||
|
||||
@@ -386,8 +386,14 @@
|
||||
<string name="answer_call">Answer</string>
|
||||
<!-- the action "to decline" a call, not: "the decline" -->
|
||||
<string name="end_call">Decline</string>
|
||||
<!-- deprecated, use outgoing_audio_call or outgoing_video_call instead -->
|
||||
<string name="outgoing_call">Outgoing call</string>
|
||||
<string name="outgoing_audio_call">Outgoing audio call</string>
|
||||
<string name="outgoing_video_call">Outgoing video call</string>
|
||||
<!-- deprecated, use incoming_audio_call or incoming_video_call instead -->
|
||||
<string name="incoming_call">Incoming call</string>
|
||||
<string name="incoming_audio_call">Incoming audio call</string>
|
||||
<string name="incoming_video_call">Incoming video call</string>
|
||||
<string name="declined_call">Declined call</string>
|
||||
<string name="canceled_call">Canceled call</string>
|
||||
<string name="missed_call">Missed call</string>
|
||||
|
||||
Reference in New Issue
Block a user