kopia lustrzana https://github.com/ryukoposting/Signal-Android
Voice Note Beta Feedback fixes.
rodzic
36da519b26
commit
082d9e852c
|
@ -14,7 +14,6 @@ import android.support.v4.media.session.MediaSessionCompat;
|
|||
import android.support.v4.media.session.PlaybackStateCompat;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
|
@ -22,7 +21,6 @@ import androidx.lifecycle.LiveData;
|
|||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
|
@ -38,6 +36,7 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver {
|
|||
|
||||
public static final String EXTRA_MESSAGE_ID = "voice.note.message_id";
|
||||
public static final String EXTRA_PLAYHEAD = "voice.note.playhead";
|
||||
public static final String EXTRA_PLAY_SINGLE = "voice.note.play.single";
|
||||
|
||||
private static final String TAG = Log.tag(VoiceNoteMediaController.class);
|
||||
|
||||
|
@ -97,15 +96,25 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver {
|
|||
return MediaControllerCompat.getMediaController(activity);
|
||||
}
|
||||
|
||||
|
||||
public void startConsecutivePlayback(@NonNull Uri audioSlideUri, long messageId, long position) {
|
||||
startPlayback(audioSlideUri, messageId, position, false);
|
||||
}
|
||||
|
||||
public void startSinglePlayback(@NonNull Uri audioSlideUri, long messageId, long position) {
|
||||
startPlayback(audioSlideUri, messageId, position, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the Media service to begin playback of a given audio slide. If the audio
|
||||
* slide is currently playing, we jump to the desired position and then begin playback.
|
||||
*
|
||||
* @param audioSlideUri The Uri of the desired audio slide
|
||||
* @param messageId The Message id of the given audio slide
|
||||
* @param position The desired position in milliseconds at which to start playback.
|
||||
* @param audioSlideUri The Uri of the desired audio slide
|
||||
* @param messageId The Message id of the given audio slide
|
||||
* @param position The desired position in milliseconds at which to start playback.
|
||||
* @param singlePlayback The player will only play back the specified Uri, and not build a playlist.
|
||||
*/
|
||||
public void startPlayback(@NonNull Uri audioSlideUri, long messageId, long position) {
|
||||
private void startPlayback(@NonNull Uri audioSlideUri, long messageId, long position, boolean singlePlayback) {
|
||||
if (isCurrentTrack(audioSlideUri)) {
|
||||
getMediaController().getTransportControls().seekTo(position);
|
||||
getMediaController().getTransportControls().play();
|
||||
|
@ -113,6 +122,7 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver {
|
|||
Bundle extras = new Bundle();
|
||||
extras.putLong(EXTRA_MESSAGE_ID, messageId);
|
||||
extras.putLong(EXTRA_PLAYHEAD, position);
|
||||
extras.putBoolean(EXTRA_PLAY_SINGLE, singlePlayback);
|
||||
|
||||
getMediaController().getTransportControls().playFromUri(audioSlideUri, extras);
|
||||
}
|
||||
|
|
|
@ -50,8 +50,8 @@ class VoiceNoteNotificationManager {
|
|||
}
|
||||
|
||||
notificationManager = PlayerNotificationManager.createWithNotificationChannel(context,
|
||||
NotificationChannels.OTHER,
|
||||
R.string.NotificationChannel_other,
|
||||
NotificationChannels.VOICE_NOTES,
|
||||
R.string.NotificationChannel_voice_notes,
|
||||
NOW_PLAYING_NOTIFICATION_ID,
|
||||
new DescriptionAdapter());
|
||||
|
||||
|
|
|
@ -88,8 +88,9 @@ final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackP
|
|||
|
||||
@Override
|
||||
public void onPrepareFromUri(final Uri uri, Bundle extras) {
|
||||
long messageId = extras.getLong(VoiceNoteMediaController.EXTRA_MESSAGE_ID);
|
||||
long position = extras.getLong(VoiceNoteMediaController.EXTRA_PLAYHEAD, 0);
|
||||
long messageId = extras.getLong(VoiceNoteMediaController.EXTRA_MESSAGE_ID);
|
||||
long position = extras.getLong(VoiceNoteMediaController.EXTRA_PLAYHEAD, 0);
|
||||
boolean singlePlayback = extras.getBoolean(VoiceNoteMediaController.EXTRA_PLAY_SINGLE, false);
|
||||
|
||||
canLoadMore = false;
|
||||
latestUri = uri;
|
||||
|
@ -98,7 +99,13 @@ final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackP
|
|||
dataSource.clear();
|
||||
|
||||
SimpleTask.run(EXECUTOR,
|
||||
() -> loadMediaDescriptions(messageId),
|
||||
() -> {
|
||||
if (singlePlayback) {
|
||||
return loadMediaDescriptionForSinglePlayback(messageId);
|
||||
} else {
|
||||
return loadMediaDescriptionsForConsecutivePlayback(messageId);
|
||||
}
|
||||
},
|
||||
descriptions -> {
|
||||
if (Util.hasItems(descriptions) && Objects.equals(latestUri, uri)) {
|
||||
applyDescriptionsToQueue(descriptions);
|
||||
|
@ -116,7 +123,7 @@ final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackP
|
|||
});
|
||||
|
||||
player.prepare(dataSource);
|
||||
canLoadMore = true;
|
||||
canLoadMore = !singlePlayback;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -203,7 +210,7 @@ final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackP
|
|||
long messageId = mediaDescriptionCompat.getExtras().getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_MESSAGE_ID);
|
||||
|
||||
SimpleTask.run(EXECUTOR,
|
||||
() -> loadMediaDescriptions(messageId),
|
||||
() -> loadMediaDescriptionsForConsecutivePlayback(messageId),
|
||||
descriptions -> {
|
||||
if (Util.hasItems(descriptions) && canLoadMore) {
|
||||
applyDescriptionsToQueue(descriptions);
|
||||
|
@ -211,8 +218,24 @@ final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackP
|
|||
});
|
||||
}
|
||||
|
||||
private @NonNull List<MediaDescriptionCompat> loadMediaDescriptionForSinglePlayback(long messageId) {
|
||||
try {
|
||||
MessageRecord messageRecord = DatabaseFactory.getMmsDatabase(context).getMessageRecord(messageId);
|
||||
|
||||
if (!MessageRecordUtil.hasAudio(messageRecord)) {
|
||||
Log.w(TAG, "Message does not contain audio.");
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return Collections.singletonList(VoiceNoteMediaDescriptionCompatFactory.buildMediaDescription(context ,messageRecord));
|
||||
} catch (NoSuchMessageException e) {
|
||||
Log.w(TAG, "Could not find message.", e);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull List<MediaDescriptionCompat> loadMediaDescriptions(long messageId) {
|
||||
private @NonNull List<MediaDescriptionCompat> loadMediaDescriptionsForConsecutivePlayback(long messageId) {
|
||||
try {
|
||||
List<MessageRecord> recordsBefore = DatabaseFactory.getMmsSmsDatabase(context).getMessagesBeforeVoiceNoteExclusive(messageId, LIMIT);
|
||||
List<MessageRecord> recordsAfter = DatabaseFactory.getMmsSmsDatabase(context).getMessagesAfterVoiceNoteInclusive(messageId, LIMIT);
|
||||
|
|
|
@ -28,6 +28,7 @@ import androidx.media.session.MediaButtonReceiver;
|
|||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.DefaultLoadControl;
|
||||
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||
import com.google.android.exoplayer2.LoadControl;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
|
@ -179,6 +180,11 @@ public class VoiceNotePlaybackService extends MediaBrowserServiceCompat {
|
|||
voiceNotePlaybackPreparer.loadMoreVoiceNotes();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerError(ExoPlaybackException error) {
|
||||
Log.w(TAG, "ExoPlayer error occurred:", error);
|
||||
}
|
||||
}
|
||||
|
||||
private class VoiceNoteNotificationManagerListener implements PlayerNotificationManager.NotificationListener {
|
||||
|
|
|
@ -118,7 +118,6 @@ import org.thoughtcrime.securesms.stickers.StickerLocator;
|
|||
import org.thoughtcrime.securesms.stickers.StickerPackPreviewActivity;
|
||||
import org.thoughtcrime.securesms.util.CachedInflater;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.HtmlUtil;
|
||||
import org.thoughtcrime.securesms.util.RemoteDeleteUtil;
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
||||
|
@ -353,9 +352,17 @@ public class ConversationFragment extends LoggingFragment {
|
|||
actionMode.finish();
|
||||
}
|
||||
|
||||
long oldThreadId = threadId;
|
||||
|
||||
initializeResources();
|
||||
messageRequestViewModel.setConversationInfo(recipient.getId(), threadId);
|
||||
initializeListAdapter();
|
||||
|
||||
int startingPosition = getStartPosition();
|
||||
if (startingPosition != -1 && oldThreadId == threadId) {
|
||||
list.post(() -> moveToPosition(startingPosition, () -> Log.w(TAG, "Could not scroll to requested message.")));
|
||||
} else {
|
||||
initializeListAdapter();
|
||||
}
|
||||
}
|
||||
|
||||
public void moveToLastSeen() {
|
||||
|
@ -373,6 +380,10 @@ public class ConversationFragment extends LoggingFragment {
|
|||
snapToTopDataObserver.requestScrollPosition(position);
|
||||
}
|
||||
|
||||
private int getStartPosition() {
|
||||
return requireActivity().getIntent().getIntExtra(ConversationActivity.STARTING_POSITION_EXTRA, -1);
|
||||
}
|
||||
|
||||
private void initializeMessageRequestViewModel() {
|
||||
MessageRequestViewModel.Factory factory = new MessageRequestViewModel.Factory(requireContext());
|
||||
|
||||
|
@ -460,7 +471,7 @@ public class ConversationFragment extends LoggingFragment {
|
|||
private void initializeResources() {
|
||||
long oldThreadId = threadId;
|
||||
|
||||
int startingPosition = this.getActivity().getIntent().getIntExtra(ConversationActivity.STARTING_POSITION_EXTRA, -1);
|
||||
int startingPosition = getStartPosition();
|
||||
|
||||
this.recipient = Recipient.live(getActivity().getIntent().getParcelableExtra(ConversationActivity.RECIPIENT_EXTRA));
|
||||
this.threadId = this.getActivity().getIntent().getLongExtra(ConversationActivity.THREAD_ID_EXTRA, -1);
|
||||
|
@ -1367,7 +1378,7 @@ public class ConversationFragment extends LoggingFragment {
|
|||
|
||||
@Override
|
||||
public void onVoiceNotePlay(@NonNull Uri uri, long messageId, long position) {
|
||||
voiceNoteMediaController.startPlayback(uri, messageId, position);
|
||||
voiceNoteMediaController.startConsecutivePlayback(uri, messageId, position);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -36,6 +36,7 @@ import org.thoughtcrime.securesms.R;
|
|||
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
||||
import org.thoughtcrime.securesms.components.AudioView;
|
||||
import org.thoughtcrime.securesms.components.ThumbnailView;
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord;
|
||||
import org.thoughtcrime.securesms.database.loaders.GroupedThreadMediaLoader.GroupedThreadMedia;
|
||||
|
@ -56,6 +57,7 @@ import java.util.HashMap;
|
|||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
|
||||
|
||||
|
@ -64,7 +66,7 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
|
|||
private final GlideRequests glideRequests;
|
||||
private final ItemClickListener itemClickListener;
|
||||
private final Map<AttachmentId, MediaRecord> selected = new HashMap<>();
|
||||
private final AudioView.Callbacks audioViewCallbacks;
|
||||
private final AudioItemListener audioItemListener;
|
||||
|
||||
private GroupedThreadMedia media;
|
||||
private boolean showFileSizes;
|
||||
|
@ -94,7 +96,7 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
|
|||
@NonNull GlideRequests glideRequests,
|
||||
GroupedThreadMedia media,
|
||||
ItemClickListener clickListener,
|
||||
@NonNull AudioView.Callbacks audioViewCallbacks,
|
||||
@NonNull AudioItemListener audioItemListener,
|
||||
boolean showFileSizes,
|
||||
boolean showThread)
|
||||
{
|
||||
|
@ -102,7 +104,7 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
|
|||
this.glideRequests = glideRequests;
|
||||
this.media = media;
|
||||
this.itemClickListener = clickListener;
|
||||
this.audioViewCallbacks = audioViewCallbacks;
|
||||
this.audioItemListener = audioItemListener;
|
||||
this.showFileSizes = showFileSizes;
|
||||
this.showThread = showThread;
|
||||
}
|
||||
|
@ -435,11 +437,22 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
|
|||
throw new AssertionError();
|
||||
}
|
||||
|
||||
audioView.setAudio((AudioSlide) slide, audioViewCallbacks, true);
|
||||
long mmsId = Objects.requireNonNull(mediaRecord.getAttachment()).getMmsId();
|
||||
|
||||
audioItemListener.unregisterPlaybackStateObserver(audioView.getPlaybackStateObserver());
|
||||
audioView.setAudio((AudioSlide) slide, new AudioViewCallbacksAdapter(audioItemListener, mmsId), true);
|
||||
audioItemListener.registerPlaybackStateObserver(audioView.getPlaybackStateObserver());
|
||||
|
||||
audioView.setOnClickListener(view -> itemClickListener.onMediaClicked(mediaRecord));
|
||||
itemView.setOnClickListener(view -> itemClickListener.onMediaClicked(mediaRecord));
|
||||
}
|
||||
|
||||
@Override
|
||||
void unbind() {
|
||||
super.unbind();
|
||||
audioItemListener.unregisterPlaybackStateObserver(audioView.getPlaybackStateObserver());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getFileTypeDescription(@NonNull Context context, @NonNull Slide slide) {
|
||||
return context.getString(R.string.MediaOverviewActivity_audio);
|
||||
|
@ -478,8 +491,48 @@ final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
|
|||
}
|
||||
}
|
||||
|
||||
private static final class AudioViewCallbacksAdapter implements AudioView.Callbacks {
|
||||
|
||||
private final AudioItemListener audioItemListener;
|
||||
private final long messageId;
|
||||
|
||||
private AudioViewCallbacksAdapter(@NonNull AudioItemListener audioItemListener, long messageId) {
|
||||
this.audioItemListener = audioItemListener;
|
||||
this.messageId = messageId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlay(@NonNull Uri audioUri, long position) {
|
||||
audioItemListener.onPlay(audioUri, position, messageId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause(@NonNull Uri audioUri) {
|
||||
audioItemListener.onPause(audioUri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSeekTo(@NonNull Uri audioUri, long position) {
|
||||
audioItemListener.onSeekTo(audioUri, position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopAndReset(@NonNull Uri audioUri) {
|
||||
audioItemListener.onStopAndReset(audioUri);
|
||||
}
|
||||
}
|
||||
|
||||
interface ItemClickListener {
|
||||
void onMediaClicked(@NonNull MediaDatabase.MediaRecord mediaRecord);
|
||||
void onMediaLongClicked(MediaDatabase.MediaRecord mediaRecord);
|
||||
}
|
||||
|
||||
interface AudioItemListener {
|
||||
void onPlay(@NonNull Uri audioUri, long position, long messageId);
|
||||
void onPause(@NonNull Uri audioUri);
|
||||
void onSeekTo(@NonNull Uri audioUri, long position);
|
||||
void onStopAndReset(@NonNull Uri audioUri);
|
||||
void registerPlaybackStateObserver(@NonNull Observer<VoiceNotePlaybackState> observer);
|
||||
void unregisterPlaybackStateObserver(@NonNull Observer<VoiceNotePlaybackState> observer);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import androidx.appcompat.app.AppCompatActivity;
|
|||
import androidx.appcompat.view.ActionMode;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
@ -31,8 +32,8 @@ import com.codewaves.stickyheadergrid.StickyHeaderGridLayoutManager;
|
|||
import org.thoughtcrime.securesms.MediaPreviewActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.components.AudioView;
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController;
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase;
|
||||
import org.thoughtcrime.securesms.database.loaders.GroupedThreadMediaLoader;
|
||||
import org.thoughtcrime.securesms.database.loaders.MediaLoader;
|
||||
|
@ -44,7 +45,7 @@ import org.thoughtcrime.securesms.util.Util;
|
|||
|
||||
public final class MediaOverviewPageFragment extends Fragment
|
||||
implements MediaGalleryAllAdapter.ItemClickListener,
|
||||
AudioView.Callbacks,
|
||||
MediaGalleryAllAdapter.AudioItemListener,
|
||||
LoaderManager.LoaderCallbacks<GroupedThreadMediaLoader.GroupedThreadMedia>
|
||||
{
|
||||
|
||||
|
@ -310,8 +311,8 @@ public final class MediaOverviewPageFragment extends Fragment
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onPlay(@NonNull Uri audioUri, long position) {
|
||||
voiceNoteMediaController.startPlayback(audioUri, -1, position);
|
||||
public void onPlay(@NonNull Uri audioUri, long position, long messageId) {
|
||||
voiceNoteMediaController.startSinglePlayback(audioUri, messageId, position);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -329,6 +330,16 @@ public final class MediaOverviewPageFragment extends Fragment
|
|||
voiceNoteMediaController.stopPlaybackAndReset(audioUri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerPlaybackStateObserver(@NonNull Observer<VoiceNotePlaybackState> observer) {
|
||||
voiceNoteMediaController.getVoiceNotePlaybackState().observe(getViewLifecycleOwner(), observer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterPlaybackStateObserver(@NonNull Observer<VoiceNotePlaybackState> observer) {
|
||||
voiceNoteMediaController.getVoiceNotePlaybackState().removeObserver(observer);
|
||||
}
|
||||
|
||||
private class ActionModeCallback implements ActionMode.Callback {
|
||||
|
||||
private int originalStatusBarColor;
|
||||
|
|
|
@ -57,6 +57,7 @@ public class NotificationChannels {
|
|||
public static final String BACKUPS = "backups_v2";
|
||||
public static final String LOCKED_STATUS = "locked_status_v2";
|
||||
public static final String OTHER = "other_v2";
|
||||
public static final String VOICE_NOTES = "voice_notes";
|
||||
|
||||
/**
|
||||
* Ensures all of the notification channels are created. No harm in repeat calls. Call is safely
|
||||
|
@ -449,6 +450,7 @@ public class NotificationChannels {
|
|||
NotificationChannel backups = new NotificationChannel(BACKUPS, context.getString(R.string.NotificationChannel_backups), NotificationManager.IMPORTANCE_LOW);
|
||||
NotificationChannel lockedStatus = new NotificationChannel(LOCKED_STATUS, context.getString(R.string.NotificationChannel_locked_status), NotificationManager.IMPORTANCE_LOW);
|
||||
NotificationChannel other = new NotificationChannel(OTHER, context.getString(R.string.NotificationChannel_other), NotificationManager.IMPORTANCE_LOW);
|
||||
NotificationChannel voiceNotes = new NotificationChannel(VOICE_NOTES, context.getString(R.string.NotificationChannel_voice_notes), NotificationManager.IMPORTANCE_LOW);
|
||||
|
||||
messages.setGroup(CATEGORY_MESSAGES);
|
||||
messages.enableVibration(TextSecurePreferences.isNotificationVibrateEnabled(context));
|
||||
|
@ -459,8 +461,9 @@ public class NotificationChannels {
|
|||
backups.setShowBadge(false);
|
||||
lockedStatus.setShowBadge(false);
|
||||
other.setShowBadge(false);
|
||||
voiceNotes.setShowBadge(false);
|
||||
|
||||
notificationManager.createNotificationChannels(Arrays.asList(messages, calls, failures, backups, lockedStatus, other));
|
||||
notificationManager.createNotificationChannels(Arrays.asList(messages, calls, failures, backups, lockedStatus, other, voiceNotes));
|
||||
|
||||
if (BuildConfig.PLAY_STORE_DISABLED) {
|
||||
NotificationChannel appUpdates = new NotificationChannel(APP_UPDATES, context.getString(R.string.NotificationChannel_app_updates), NotificationManager.IMPORTANCE_HIGH);
|
||||
|
|
|
@ -1587,6 +1587,7 @@
|
|||
<string name="NotificationChannel_other">Other</string>
|
||||
<string name="NotificationChannel_group_messages">Messages</string>
|
||||
<string name="NotificationChannel_missing_display_name">Unknown</string>
|
||||
<string name="NotificationChannel_voice_notes">Voice Notes</string>
|
||||
|
||||
<!-- ProfileEditNameFragment -->
|
||||
<string name="ProfileEditNameFragment_successfully_set_profile_name">Successfully set profile name.</string>
|
||||
|
|
Ładowanie…
Reference in New Issue