mirror of
https://github.com/ArcaneChat/android.git
synced 2026-07-03 14:05:24 +02:00
Merge branch 'main' into wch423/call-notifiaction
This commit is contained in:
@@ -1179,6 +1179,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
playbackViewModel.stopNonMessageAudioPlayback();
|
||||
|
||||
DcContext dcContext = DcHelper.getContext(context);
|
||||
final int currentChatId = dcChat.getId();
|
||||
Util.runOnAnyBackgroundThread(() -> {
|
||||
DcMsg msg = null;
|
||||
int recompress = 0;
|
||||
@@ -1188,9 +1189,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
if (action == ACTION_SEND_OUT) {
|
||||
dcContext.sendEditRequest(msgId, body);
|
||||
} else {
|
||||
dcContext.setDraft(chatId, null);
|
||||
dcContext.setDraft(currentChatId, null);
|
||||
}
|
||||
future.set(chatId);
|
||||
future.set(currentChatId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1201,7 +1202,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
try {
|
||||
if (slideDeck.getWebxdctDraftId() != 0) {
|
||||
msg = dcContext.getDraft(chatId);
|
||||
msg = dcContext.getDraft(currentChatId);
|
||||
} else {
|
||||
List<Attachment> attachments = slideDeck.asAttachments();
|
||||
for (Attachment attachment : attachments) {
|
||||
@@ -1246,7 +1247,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
// for WEBXDC, drafts are just sent out as is.
|
||||
// for preparations and other cases, cleanup draft soon.
|
||||
if (msg == null || msg.getType() != DcMsg.DC_MSG_WEBXDC) {
|
||||
dcContext.setDraft(dcChat.getId(), null);
|
||||
dcContext.setDraft(currentChatId, null);
|
||||
}
|
||||
|
||||
if(msg!=null) {
|
||||
@@ -1262,7 +1263,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
false
|
||||
);
|
||||
});
|
||||
doSend = VideoRecoder.prepareVideo(ConversationActivity.this, dcChat.getId(), msg);
|
||||
doSend = VideoRecoder.prepareVideo(ConversationActivity.this, currentChatId, msg);
|
||||
Util.runOnMain(() -> {
|
||||
try {
|
||||
if (progressDialog != null) progressDialog.dismiss();
|
||||
@@ -1273,48 +1274,36 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
|
||||
if (doSend) {
|
||||
if (dcContext.sendMsg(dcChat.getId(), msg) == 0) {
|
||||
if (dcContext.sendMsg(currentChatId, msg) == 0) {
|
||||
String lastError = dcContext.getLastError();
|
||||
if (!"".equals(lastError)) {
|
||||
Util.runOnMain(() -> Toast.makeText(ConversationActivity.this, lastError, Toast.LENGTH_LONG).show());
|
||||
}
|
||||
future.set(chatId);
|
||||
future.set(currentChatId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Util.runOnMain(() -> sendComplete(dcChat.getId()));
|
||||
if (currentChatId == this.chatId) {
|
||||
Util.runOnMain(() -> sendComplete());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
dcContext.setDraft(dcChat.getId(), msg);
|
||||
dcContext.setDraft(currentChatId, msg);
|
||||
}
|
||||
future.set(chatId);
|
||||
future.set(currentChatId);
|
||||
});
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
|
||||
protected void sendComplete(int chatId) {
|
||||
boolean refreshFragment = (chatId != this.chatId);
|
||||
this.chatId = chatId;
|
||||
|
||||
protected void sendComplete() {
|
||||
if (fragment == null || !fragment.isVisible() || isFinishing()) {
|
||||
return;
|
||||
}
|
||||
|
||||
fragment.setLastSeen(-1);
|
||||
|
||||
if (refreshFragment) {
|
||||
fragment.reload(recipient, chatId);
|
||||
try {
|
||||
int accId = rpc.getSelectedAccountId();
|
||||
DcHelper.getNotificationCenter(this).updateVisibleChat(accId, chatId);
|
||||
} catch (RpcException e) {
|
||||
Log.e(TAG, "rpc.getSelectedAccountId() failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
fragment.scrollToBottom();
|
||||
attachmentManager.cleanup();
|
||||
}
|
||||
@@ -1376,7 +1365,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
future.addListener(new ListenableFuture.Listener<Pair<Uri, Long>>() {
|
||||
@Override
|
||||
public void onSuccess(final @NonNull Pair<Uri, Long> result) {
|
||||
AudioSlide audioSlide = new AudioSlide(ConversationActivity.this, result.first, result.second, MediaUtil.AUDIO_AAC, true);
|
||||
AudioSlide audioSlide = new AudioSlide(ConversationActivity.this, result.first, result.second, MediaUtil.AUDIO_M4A, true);
|
||||
SlideDeck slideDeck = new SlideDeck();
|
||||
slideDeck.addSlide(audioSlide);
|
||||
|
||||
|
||||
@@ -6,20 +6,18 @@ import android.media.AudioRecord;
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaCodecInfo;
|
||||
import android.media.MediaFormat;
|
||||
import android.media.MediaMuxer;
|
||||
import android.media.MediaRecorder;
|
||||
import android.util.Log;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import org.thoughtcrime.securesms.util.Prefs;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
public class AudioCodec {
|
||||
|
||||
private static final String TAG = AudioCodec.class.getSimpleName();
|
||||
|
||||
private static final int SAMPLE_RATE = 44100;
|
||||
private static final int SAMPLE_RATE_INDEX = 4;
|
||||
private static final int CHANNELS = 1;
|
||||
private static final int BIT_RATE_BALANCED = 32000;
|
||||
private static final int BIT_RATE_WORSE = 24000;
|
||||
@@ -27,11 +25,14 @@ public class AudioCodec {
|
||||
private final int bufferSize;
|
||||
private final MediaCodec mediaCodec;
|
||||
private final AudioRecord audioRecord;
|
||||
private final String outputPath;
|
||||
|
||||
private boolean running = true;
|
||||
private boolean finished = false;
|
||||
private volatile boolean running = true;
|
||||
private volatile boolean finished = false;
|
||||
private long startTime = 0;
|
||||
|
||||
public AudioCodec(Context context) throws IOException {
|
||||
public AudioCodec(Context context, String outputPath) throws IOException {
|
||||
this.outputPath = outputPath;
|
||||
this.bufferSize =
|
||||
AudioRecord.getMinBufferSize(
|
||||
SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
|
||||
@@ -43,7 +44,7 @@ public class AudioCodec {
|
||||
try {
|
||||
audioRecord.startRecording();
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, e);
|
||||
Log.w(TAG, "Failed to start recording", e);
|
||||
mediaCodec.release();
|
||||
throw new IOException(e);
|
||||
}
|
||||
@@ -51,41 +52,147 @@ public class AudioCodec {
|
||||
|
||||
public synchronized void stop() {
|
||||
running = false;
|
||||
while (!finished) Util.wait(this, 0);
|
||||
while (!finished) {
|
||||
try {
|
||||
wait(5000);
|
||||
if (!finished) {
|
||||
Log.w(TAG, "Timeout waiting for recording to finish");
|
||||
break;
|
||||
}
|
||||
} catch (InterruptedException ie) {
|
||||
Log.w(TAG, "Interrupted while waiting for recording to finish", ie);
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void start(final OutputStream outputStream) {
|
||||
public void start() {
|
||||
new Thread(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
||||
byte[] audioRecordData = new byte[bufferSize];
|
||||
ByteBuffer[] codecInputBuffers = mediaCodec.getInputBuffers();
|
||||
ByteBuffer[] codecOutputBuffers = mediaCodec.getOutputBuffers();
|
||||
() -> {
|
||||
this.startTime = System.nanoTime();
|
||||
|
||||
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
||||
byte[] audioRecordData = new byte[bufferSize];
|
||||
MediaMuxer muxer = null;
|
||||
int audioTrackIndex = -1;
|
||||
boolean muxerStarted = false;
|
||||
|
||||
try {
|
||||
muxer = new MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
|
||||
|
||||
while (true) {
|
||||
boolean running = isRunning();
|
||||
|
||||
handleCodecInput(audioRecord, audioRecordData, mediaCodec, running);
|
||||
int codecOutputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 2000);
|
||||
|
||||
while (codecOutputBufferIndex != MediaCodec.INFO_TRY_AGAIN_LATER) {
|
||||
if (codecOutputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
|
||||
// Get the format to add track to muxer
|
||||
if (muxerStarted) {
|
||||
Log.w(TAG, "Output format changed after muxer started, ignoring");
|
||||
codecOutputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);
|
||||
continue;
|
||||
}
|
||||
MediaFormat newFormat = mediaCodec.getOutputFormat();
|
||||
Log.d(TAG, "Output format changed: " + newFormat);
|
||||
audioTrackIndex = muxer.addTrack(newFormat);
|
||||
muxer.start();
|
||||
muxerStarted = true;
|
||||
Log.d(TAG, "Muxer started");
|
||||
} else if (codecOutputBufferIndex >= 0) {
|
||||
ByteBuffer encodedData = mediaCodec.getOutputBuffer(codecOutputBufferIndex);
|
||||
|
||||
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
|
||||
// Codec config info, not actual data, skip it
|
||||
Log.d(TAG, "Ignoring CODEC_CONFIG buffer");
|
||||
bufferInfo.size = 0;
|
||||
}
|
||||
|
||||
if (bufferInfo.size != 0 && encodedData != null) {
|
||||
if (!muxerStarted) {
|
||||
Log.w(TAG, "Muxer not started, dropping encoded data");
|
||||
} else {
|
||||
encodedData.position(bufferInfo.offset);
|
||||
encodedData.limit(bufferInfo.offset + bufferInfo.size);
|
||||
muxer.writeSampleData(audioTrackIndex, encodedData, bufferInfo);
|
||||
}
|
||||
|
||||
// Adjust ByteBuffer to match BufferInfo
|
||||
encodedData.position(bufferInfo.offset);
|
||||
encodedData.limit(bufferInfo.offset + bufferInfo.size);
|
||||
|
||||
// Write sample data to muxer
|
||||
muxer.writeSampleData(audioTrackIndex, encodedData, bufferInfo);
|
||||
}
|
||||
|
||||
mediaCodec.releaseOutputBuffer(codecOutputBufferIndex, false);
|
||||
|
||||
// Check for end of stream
|
||||
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
|
||||
Log.d(TAG, "End of stream reached");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
codecOutputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);
|
||||
}
|
||||
|
||||
if (!running && (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "Error during encoding", e);
|
||||
} finally {
|
||||
try {
|
||||
if (muxerStarted && muxer != null) {
|
||||
try {
|
||||
muxer.stop();
|
||||
Log.d(TAG, "Muxer stopped");
|
||||
} catch (IllegalStateException e) {
|
||||
Log.w(TAG, "Muxer already stopped", e);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "Error stopping muxer", e);
|
||||
}
|
||||
|
||||
if (muxer != null) {
|
||||
try {
|
||||
muxer.release();
|
||||
Log.d(TAG, "Muxer released");
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "Error releasing muxer", e);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
boolean running = isRunning();
|
||||
|
||||
handleCodecInput(
|
||||
audioRecord, audioRecordData, mediaCodec, codecInputBuffers, running);
|
||||
handleCodecOutput(mediaCodec, codecOutputBuffers, bufferInfo, outputStream);
|
||||
|
||||
if (!running) break;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
} finally {
|
||||
mediaCodec.stop();
|
||||
audioRecord.stop();
|
||||
|
||||
mediaCodec.release();
|
||||
audioRecord.release();
|
||||
|
||||
Util.close(outputStream);
|
||||
setFinished();
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "Error stopping codec", e);
|
||||
}
|
||||
|
||||
try {
|
||||
audioRecord.stop();
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "Error stopping audio record", e);
|
||||
}
|
||||
|
||||
try {
|
||||
mediaCodec.release();
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "Error releasing codec", e);
|
||||
}
|
||||
|
||||
try {
|
||||
audioRecord.release();
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "Error releasing audio record", e);
|
||||
}
|
||||
|
||||
setFinished();
|
||||
}
|
||||
},
|
||||
AudioCodec.class.getSimpleName())
|
||||
@@ -102,75 +209,35 @@ public class AudioCodec {
|
||||
}
|
||||
|
||||
private void handleCodecInput(
|
||||
AudioRecord audioRecord,
|
||||
byte[] audioRecordData,
|
||||
MediaCodec mediaCodec,
|
||||
ByteBuffer[] codecInputBuffers,
|
||||
boolean running) {
|
||||
AudioRecord audioRecord, byte[] audioRecordData, MediaCodec mediaCodec, boolean running) {
|
||||
int length = audioRecord.read(audioRecordData, 0, audioRecordData.length);
|
||||
|
||||
if (length < 0) {
|
||||
Log.w(TAG, "Error reading from AudioRecord: " + length);
|
||||
return;
|
||||
}
|
||||
|
||||
int codecInputBufferIndex = mediaCodec.dequeueInputBuffer(10 * 1000);
|
||||
|
||||
if (codecInputBufferIndex >= 0) {
|
||||
ByteBuffer codecBuffer = codecInputBuffers[codecInputBufferIndex];
|
||||
codecBuffer.clear();
|
||||
codecBuffer.put(audioRecordData);
|
||||
mediaCodec.queueInputBuffer(
|
||||
codecInputBufferIndex, 0, length, 0, running ? 0 : MediaCodec.BUFFER_FLAG_END_OF_STREAM);
|
||||
}
|
||||
}
|
||||
ByteBuffer codecBuffer = mediaCodec.getInputBuffer(codecInputBufferIndex);
|
||||
|
||||
private void handleCodecOutput(
|
||||
MediaCodec mediaCodec,
|
||||
ByteBuffer[] codecOutputBuffers,
|
||||
MediaCodec.BufferInfo bufferInfo,
|
||||
OutputStream outputStream)
|
||||
throws IOException {
|
||||
int codecOutputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);
|
||||
|
||||
while (codecOutputBufferIndex != MediaCodec.INFO_TRY_AGAIN_LATER) {
|
||||
if (codecOutputBufferIndex >= 0) {
|
||||
ByteBuffer encoderOutputBuffer = codecOutputBuffers[codecOutputBufferIndex];
|
||||
|
||||
encoderOutputBuffer.position(bufferInfo.offset);
|
||||
encoderOutputBuffer.limit(bufferInfo.offset + bufferInfo.size);
|
||||
|
||||
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG)
|
||||
!= MediaCodec.BUFFER_FLAG_CODEC_CONFIG) {
|
||||
byte[] header = createAdtsHeader(bufferInfo.size - bufferInfo.offset);
|
||||
|
||||
outputStream.write(header);
|
||||
|
||||
byte[] data = new byte[encoderOutputBuffer.remaining()];
|
||||
encoderOutputBuffer.get(data);
|
||||
outputStream.write(data);
|
||||
}
|
||||
|
||||
encoderOutputBuffer.clear();
|
||||
|
||||
mediaCodec.releaseOutputBuffer(codecOutputBufferIndex, false);
|
||||
} else if (codecOutputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
|
||||
codecOutputBuffers = mediaCodec.getOutputBuffers();
|
||||
if (codecBuffer != null) {
|
||||
codecBuffer.clear();
|
||||
codecBuffer.put(audioRecordData, 0, length);
|
||||
long presentationTimeUs = getPresentationTimeUs();
|
||||
mediaCodec.queueInputBuffer(
|
||||
codecInputBufferIndex,
|
||||
0,
|
||||
length,
|
||||
presentationTimeUs,
|
||||
running ? 0 : MediaCodec.BUFFER_FLAG_END_OF_STREAM);
|
||||
}
|
||||
|
||||
codecOutputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] createAdtsHeader(int length) {
|
||||
int frameLength = length + 7;
|
||||
byte[] adtsHeader = new byte[7];
|
||||
|
||||
adtsHeader[0] = (byte) 0xFF; // Sync Word
|
||||
adtsHeader[1] = (byte) 0xF1; // MPEG-4, Layer (0), No CRC
|
||||
adtsHeader[2] = (byte) ((MediaCodecInfo.CodecProfileLevel.AACObjectLC - 1) << 6);
|
||||
adtsHeader[2] |= (((byte) SAMPLE_RATE_INDEX) << 2);
|
||||
adtsHeader[2] |= (((byte) CHANNELS) >> 2);
|
||||
adtsHeader[3] = (byte) (((CHANNELS & 3) << 6) | ((frameLength >> 11) & 0x03));
|
||||
adtsHeader[4] = (byte) ((frameLength >> 3) & 0xFF);
|
||||
adtsHeader[5] = (byte) (((frameLength & 0x07) << 5) | 0x1f);
|
||||
adtsHeader[6] = (byte) 0xFC;
|
||||
|
||||
return adtsHeader;
|
||||
private long getPresentationTimeUs() {
|
||||
return (System.nanoTime() - startTime) / 1000;
|
||||
}
|
||||
|
||||
private AudioRecord createAudioRecord(int bufferSize) {
|
||||
@@ -184,11 +251,9 @@ public class AudioCodec {
|
||||
|
||||
private MediaCodec createMediaCodec(Context context, int bufferSize) throws IOException {
|
||||
MediaCodec mediaCodec = MediaCodec.createEncoderByType("audio/mp4a-latm");
|
||||
MediaFormat mediaFormat = new MediaFormat();
|
||||
|
||||
mediaFormat.setString(MediaFormat.KEY_MIME, "audio/mp4a-latm");
|
||||
mediaFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, SAMPLE_RATE);
|
||||
mediaFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, CHANNELS);
|
||||
MediaFormat mediaFormat =
|
||||
MediaFormat.createAudioFormat("audio/mp4a-latm", SAMPLE_RATE, CHANNELS);
|
||||
mediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, bufferSize);
|
||||
mediaFormat.setInteger(
|
||||
MediaFormat.KEY_BIT_RATE,
|
||||
|
||||
@@ -2,14 +2,15 @@ package org.thoughtcrime.securesms.audio;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
import androidx.annotation.NonNull;
|
||||
import chat.delta.util.ListenableFuture;
|
||||
import chat.delta.util.SettableFuture;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.ThreadUtil;
|
||||
@@ -24,8 +25,17 @@ public class AudioRecorder {
|
||||
private final Context context;
|
||||
private final PersistentBlobProvider blobProvider;
|
||||
|
||||
private AudioCodec audioCodec;
|
||||
private Uri captureUri;
|
||||
private final AtomicReference<RecordingState> recordingState = new AtomicReference<>(null);
|
||||
|
||||
private static class RecordingState {
|
||||
final AudioCodec audioCodec;
|
||||
final String outputFilePath;
|
||||
|
||||
RecordingState(AudioCodec audioCodec, String outputFilePath) {
|
||||
this.audioCodec = audioCodec;
|
||||
this.outputFilePath = outputFilePath;
|
||||
}
|
||||
}
|
||||
|
||||
public AudioRecorder(@NonNull Context context) {
|
||||
this.context = context;
|
||||
@@ -39,24 +49,22 @@ public class AudioRecorder {
|
||||
() -> {
|
||||
Log.w(TAG, "Running startRecording() + " + Thread.currentThread().getId());
|
||||
try {
|
||||
if (audioCodec != null) {
|
||||
RecordingState currentState = recordingState.get();
|
||||
if (currentState != null) {
|
||||
throw new AssertionError("We can only record once at a time.");
|
||||
}
|
||||
|
||||
ParcelFileDescriptor[] fds = ParcelFileDescriptor.createPipe();
|
||||
// Create temporary file for M4A output
|
||||
File outputFile = File.createTempFile("voice", ".m4a", context.getCacheDir());
|
||||
String outputFilePath = outputFile.getAbsolutePath();
|
||||
|
||||
captureUri =
|
||||
blobProvider.create(
|
||||
context,
|
||||
new ParcelFileDescriptor.AutoCloseInputStream(fds[0]),
|
||||
MediaUtil.AUDIO_AAC,
|
||||
"voice.aac",
|
||||
null);
|
||||
audioCodec = new AudioCodec(context);
|
||||
AudioCodec audioCodec = new AudioCodec(context, outputFilePath);
|
||||
recordingState.set(new RecordingState(audioCodec, outputFilePath));
|
||||
|
||||
audioCodec.start(new ParcelFileDescriptor.AutoCloseOutputStream(fds[1]));
|
||||
audioCodec.start();
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
recordingState.set(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -68,24 +76,39 @@ public class AudioRecorder {
|
||||
|
||||
executor.execute(
|
||||
() -> {
|
||||
if (audioCodec == null) {
|
||||
RecordingState state = recordingState.getAndSet(null);
|
||||
|
||||
if (state == null || state.audioCodec == null) {
|
||||
sendToFuture(
|
||||
future, new IOException("MediaRecorder was never initialized successfully!"));
|
||||
return;
|
||||
}
|
||||
|
||||
audioCodec.stop();
|
||||
state.audioCodec.stop();
|
||||
|
||||
try {
|
||||
long size = MediaUtil.getMediaSize(context, captureUri);
|
||||
File outputFile = new File(state.outputFilePath);
|
||||
|
||||
if (!outputFile.exists()) {
|
||||
sendToFuture(future, new IOException("Output file does not exist"));
|
||||
return;
|
||||
}
|
||||
|
||||
long size = outputFile.length();
|
||||
|
||||
// Create blob using synchronous file-based method
|
||||
Uri captureUri =
|
||||
blobProvider.create(context, outputFile, MediaUtil.AUDIO_M4A, "voice.m4a", size);
|
||||
|
||||
if (!outputFile.delete()) {
|
||||
Log.d(TAG, "Temp file already moved or couldn't be deleted");
|
||||
}
|
||||
|
||||
sendToFuture(future, new Pair<>(captureUri, size));
|
||||
} catch (IOException ioe) {
|
||||
Log.w(TAG, ioe);
|
||||
Log.w(TAG, "Failed to create blob from recording", ioe);
|
||||
sendToFuture(future, ioe);
|
||||
}
|
||||
|
||||
audioCodec = null;
|
||||
captureUri = null;
|
||||
});
|
||||
|
||||
return future;
|
||||
|
||||
@@ -12,17 +12,19 @@ import android.provider.Settings;
|
||||
import android.util.Log;
|
||||
import android.webkit.MimeTypeMap;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.content.FileProvider;
|
||||
|
||||
import chat.delta.rpc.Rpc;
|
||||
import chat.delta.rpc.RpcException;
|
||||
import com.b44t.messenger.DcAccounts;
|
||||
import com.b44t.messenger.DcChat;
|
||||
import com.b44t.messenger.DcContext;
|
||||
import com.b44t.messenger.DcLot;
|
||||
import com.b44t.messenger.DcMsg;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.BuildConfig;
|
||||
import org.thoughtcrime.securesms.LocalHelpActivity;
|
||||
@@ -38,83 +40,79 @@ import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.ShareUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
|
||||
import chat.delta.rpc.Rpc;
|
||||
import chat.delta.rpc.RpcException;
|
||||
|
||||
public class DcHelper {
|
||||
|
||||
private static final String TAG = DcHelper.class.getSimpleName();
|
||||
private static final String TAG = DcHelper.class.getSimpleName();
|
||||
|
||||
public static final String CONFIG_CONFIGURED_ADDRESS = "configured_addr";
|
||||
public static final String CONFIG_DISPLAY_NAME = "displayname";
|
||||
public static final String CONFIG_SELF_STATUS = "selfstatus";
|
||||
public static final String CONFIG_SELF_AVATAR = "selfavatar";
|
||||
public static final String CONFIG_MVBOX_MOVE = "mvbox_move";
|
||||
public static final String CONFIG_ONLY_FETCH_MVBOX = "only_fetch_mvbox";
|
||||
public static final String CONFIG_BCC_SELF = "bcc_self";
|
||||
public static final String CONFIG_SHOW_EMAILS = "show_emails";
|
||||
public static final String CONFIG_MEDIA_QUALITY = "media_quality";
|
||||
public static final String CONFIG_PROXY_ENABLED = "proxy_enabled";
|
||||
public static final String CONFIG_PROXY_URL = "proxy_url";
|
||||
public static final String CONFIG_PRIVATE_TAG = "private_tag";
|
||||
public static final String CONFIG_STATS_SENDING = "stats_sending";
|
||||
public static final String CONFIG_STATS_ID = "stats_id";
|
||||
public static final String CONFIG_CONFIGURED_ADDRESS = "configured_addr";
|
||||
public static final String CONFIG_DISPLAY_NAME = "displayname";
|
||||
public static final String CONFIG_SELF_STATUS = "selfstatus";
|
||||
public static final String CONFIG_SELF_AVATAR = "selfavatar";
|
||||
public static final String CONFIG_MVBOX_MOVE = "mvbox_move";
|
||||
public static final String CONFIG_ONLY_FETCH_MVBOX = "only_fetch_mvbox";
|
||||
public static final String CONFIG_BCC_SELF = "bcc_self";
|
||||
public static final String CONFIG_SHOW_EMAILS = "show_emails";
|
||||
public static final String CONFIG_MEDIA_QUALITY = "media_quality";
|
||||
public static final String CONFIG_PROXY_ENABLED = "proxy_enabled";
|
||||
public static final String CONFIG_PROXY_URL = "proxy_url";
|
||||
public static final String CONFIG_PRIVATE_TAG = "private_tag";
|
||||
public static final String CONFIG_STATS_SENDING = "stats_sending";
|
||||
public static final String CONFIG_STATS_ID = "stats_id";
|
||||
|
||||
public static DcContext getContext(@NonNull Context context) {
|
||||
return ApplicationContext.getInstance(context).getDcContext();
|
||||
}
|
||||
public static DcContext getContext(@NonNull Context context) {
|
||||
return ApplicationContext.getInstance(context).getDcContext();
|
||||
}
|
||||
|
||||
public static Rpc getRpc(@NonNull Context context) {
|
||||
return ApplicationContext.getInstance(context).getRpc();
|
||||
}
|
||||
public static Rpc getRpc(@NonNull Context context) {
|
||||
return ApplicationContext.getInstance(context).getRpc();
|
||||
}
|
||||
|
||||
public static DcAccounts getAccounts(@NonNull Context context) {
|
||||
return ApplicationContext.getInstance(context).getDcAccounts();
|
||||
}
|
||||
public static DcAccounts getAccounts(@NonNull Context context) {
|
||||
return ApplicationContext.getInstance(context).getDcAccounts();
|
||||
}
|
||||
|
||||
public static DcEventCenter getEventCenter(@NonNull Context context) {
|
||||
return ApplicationContext.getInstance(context).getEventCenter();
|
||||
}
|
||||
public static DcEventCenter getEventCenter(@NonNull Context context) {
|
||||
return ApplicationContext.getInstance(context).getEventCenter();
|
||||
}
|
||||
|
||||
public static NotificationCenter getNotificationCenter(@NonNull Context context) {
|
||||
return ApplicationContext.getInstance(context).getNotificationCenter();
|
||||
}
|
||||
public static NotificationCenter getNotificationCenter(@NonNull Context context) {
|
||||
return ApplicationContext.getInstance(context).getNotificationCenter();
|
||||
}
|
||||
|
||||
public static boolean isConfigured(Context context) {
|
||||
DcContext dcContext = getContext(context);
|
||||
return dcContext.isConfigured() == 1;
|
||||
}
|
||||
public static boolean isConfigured(Context context) {
|
||||
DcContext dcContext = getContext(context);
|
||||
return dcContext.isConfigured() == 1;
|
||||
}
|
||||
|
||||
public static int getInt(Context context, String key) {
|
||||
DcContext dcContext = getContext(context);
|
||||
return dcContext.getConfigInt(key);
|
||||
}
|
||||
public static int getInt(Context context, String key) {
|
||||
DcContext dcContext = getContext(context);
|
||||
return dcContext.getConfigInt(key);
|
||||
}
|
||||
|
||||
public static String get(Context context, String key) {
|
||||
DcContext dcContext = getContext(context);
|
||||
return dcContext.getConfig(key);
|
||||
}
|
||||
public static String get(Context context, String key) {
|
||||
DcContext dcContext = getContext(context);
|
||||
return dcContext.getConfig(key);
|
||||
}
|
||||
|
||||
@Deprecated public static int getInt(Context context, String key, int defaultValue) {
|
||||
return getInt(context, key);
|
||||
}
|
||||
@Deprecated
|
||||
public static int getInt(Context context, String key, int defaultValue) {
|
||||
return getInt(context, key);
|
||||
}
|
||||
|
||||
@Deprecated public static String get(Context context, String key, String defaultValue) {
|
||||
return get(context, key);
|
||||
}
|
||||
@Deprecated
|
||||
public static String get(Context context, String key, String defaultValue) {
|
||||
return get(context, key);
|
||||
}
|
||||
|
||||
public static void set(Context context, String key, String value) {
|
||||
DcContext dcContext = getContext(context);
|
||||
dcContext.setConfig(key, value);
|
||||
}
|
||||
public static void set(Context context, String key, String value) {
|
||||
DcContext dcContext = getContext(context);
|
||||
dcContext.setConfig(key, value);
|
||||
}
|
||||
|
||||
public static void setStockTranslations(Context context) {
|
||||
DcContext dcContext = getContext(context);
|
||||
// the integers are defined in the core and used only here, an enum or sth. like that won't have a big benefit
|
||||
// the integers are defined in the core and used only here, an enum or sth. like that won't have
|
||||
// a big benefit
|
||||
dcContext.setStockTranslation(1, context.getString(R.string.chat_no_messages));
|
||||
dcContext.setStockTranslation(2, context.getString(R.string.self));
|
||||
dcContext.setStockTranslation(3, context.getString(R.string.draft));
|
||||
@@ -133,7 +131,8 @@ public class DcHelper {
|
||||
dcContext.setStockTranslation(69, context.getString(R.string.saved_messages));
|
||||
dcContext.setStockTranslation(70, context.getString(R.string.device_talk_explain));
|
||||
dcContext.setStockTranslation(71, context.getString(R.string.device_talk_welcome_message2));
|
||||
dcContext.setStockTranslation(73, context.getString(R.string.systemmsg_subject_for_new_contact));
|
||||
dcContext.setStockTranslation(
|
||||
73, context.getString(R.string.systemmsg_subject_for_new_contact));
|
||||
dcContext.setStockTranslation(74, context.getString(R.string.systemmsg_failed_sending_to));
|
||||
dcContext.setStockTranslation(84, context.getString(R.string.configuration_failed_with_error));
|
||||
dcContext.setStockTranslation(85, context.getString(R.string.devicemsg_bad_time));
|
||||
@@ -160,9 +159,11 @@ public class DcHelper {
|
||||
dcContext.setStockTranslation(119, context.getString(R.string.qrshow_join_contact_hint));
|
||||
|
||||
// HACK: svg does not handle entities correctly and shows `"` as the text `quot;`.
|
||||
// until that is fixed, we fix the most obvious errors (core uses encode_minimal, so this does not affect so many characters)
|
||||
// until that is fixed, we fix the most obvious errors (core uses encode_minimal, so this does
|
||||
// not affect so many characters)
|
||||
// cmp. https://github.com/deltachat/deltachat-android/issues/2187
|
||||
dcContext.setStockTranslation(120, context.getString(R.string.qrshow_join_group_hint).replace("\"", ""));
|
||||
dcContext.setStockTranslation(
|
||||
120, context.getString(R.string.qrshow_join_group_hint).replace("\"", ""));
|
||||
dcContext.setStockTranslation(121, context.getString(R.string.connectivity_not_connected));
|
||||
dcContext.setStockTranslation(124, context.getString(R.string.group_name_changed_by_you));
|
||||
dcContext.setStockTranslation(125, context.getString(R.string.group_name_changed_by_other));
|
||||
@@ -179,9 +180,11 @@ public class DcHelper {
|
||||
dcContext.setStockTranslation(136, context.getString(R.string.location_enabled_by_you));
|
||||
dcContext.setStockTranslation(137, context.getString(R.string.location_enabled_by_other));
|
||||
dcContext.setStockTranslation(138, context.getString(R.string.ephemeral_timer_disabled_by_you));
|
||||
dcContext.setStockTranslation(139, context.getString(R.string.ephemeral_timer_disabled_by_other));
|
||||
dcContext.setStockTranslation(
|
||||
139, context.getString(R.string.ephemeral_timer_disabled_by_other));
|
||||
dcContext.setStockTranslation(140, context.getString(R.string.ephemeral_timer_seconds_by_you));
|
||||
dcContext.setStockTranslation(141, context.getString(R.string.ephemeral_timer_seconds_by_other));
|
||||
dcContext.setStockTranslation(
|
||||
141, context.getString(R.string.ephemeral_timer_seconds_by_other));
|
||||
dcContext.setStockTranslation(144, context.getString(R.string.ephemeral_timer_1_hour_by_you));
|
||||
dcContext.setStockTranslation(145, context.getString(R.string.ephemeral_timer_1_hour_by_other));
|
||||
dcContext.setStockTranslation(146, context.getString(R.string.ephemeral_timer_1_day_by_you));
|
||||
@@ -189,7 +192,8 @@ public class DcHelper {
|
||||
dcContext.setStockTranslation(148, context.getString(R.string.ephemeral_timer_1_week_by_you));
|
||||
dcContext.setStockTranslation(149, context.getString(R.string.ephemeral_timer_1_week_by_other));
|
||||
dcContext.setStockTranslation(150, context.getString(R.string.ephemeral_timer_minutes_by_you));
|
||||
dcContext.setStockTranslation(151, context.getString(R.string.ephemeral_timer_minutes_by_other));
|
||||
dcContext.setStockTranslation(
|
||||
151, context.getString(R.string.ephemeral_timer_minutes_by_other));
|
||||
dcContext.setStockTranslation(152, context.getString(R.string.ephemeral_timer_hours_by_you));
|
||||
dcContext.setStockTranslation(153, context.getString(R.string.ephemeral_timer_hours_by_other));
|
||||
dcContext.setStockTranslation(154, context.getString(R.string.ephemeral_timer_days_by_you));
|
||||
@@ -199,11 +203,14 @@ public class DcHelper {
|
||||
dcContext.setStockTranslation(158, context.getString(R.string.ephemeral_timer_1_year_by_you));
|
||||
dcContext.setStockTranslation(159, context.getString(R.string.ephemeral_timer_1_year_by_other));
|
||||
dcContext.setStockTranslation(162, context.getString(R.string.multidevice_qr_subtitle));
|
||||
dcContext.setStockTranslation(163, context.getString(R.string.multidevice_transfer_done_devicemsg));
|
||||
dcContext.setStockTranslation(170, context.getString(R.string.chat_protection_enabled_tap_to_learn_more));
|
||||
dcContext.setStockTranslation(
|
||||
163, context.getString(R.string.multidevice_transfer_done_devicemsg));
|
||||
dcContext.setStockTranslation(
|
||||
170, context.getString(R.string.chat_protection_enabled_tap_to_learn_more));
|
||||
dcContext.setStockTranslation(172, context.getString(R.string.chat_new_group_hint));
|
||||
dcContext.setStockTranslation(173, context.getString(R.string.member_x_added));
|
||||
dcContext.setStockTranslation(174, context.getString(R.string.invalid_unencrypted_tap_to_learn_more));
|
||||
dcContext.setStockTranslation(
|
||||
174, context.getString(R.string.invalid_unencrypted_tap_to_learn_more));
|
||||
dcContext.setStockTranslation(176, context.getString(R.string.reaction_by_you));
|
||||
dcContext.setStockTranslation(177, context.getString(R.string.reaction_by_other));
|
||||
dcContext.setStockTranslation(178, context.getString(R.string.member_x_removed));
|
||||
@@ -225,28 +232,34 @@ public class DcHelper {
|
||||
dcContext.setStockTranslation(234, context.getString(R.string.audio_call));
|
||||
dcContext.setStockTranslation(235, context.getString(R.string.video_call));
|
||||
dcContext.setStockTranslation(240, context.getString(R.string.chat_description_changed_by_you));
|
||||
dcContext.setStockTranslation(241, context.getString(R.string.chat_description_changed_by_other));
|
||||
dcContext.setStockTranslation(
|
||||
241, context.getString(R.string.chat_description_changed_by_other));
|
||||
}
|
||||
|
||||
public static File getImexDir() {
|
||||
// DIRECTORY_DOCUMENTS could be used but DIRECTORY_DOWNLOADS seems to be easier accessible by the user,
|
||||
// DIRECTORY_DOCUMENTS could be used but DIRECTORY_DOWNLOADS seems to be easier accessible by
|
||||
// the user,
|
||||
// eg. "Download Managers" are nearly always installed.
|
||||
// CAVE: do not use DownloadManager to add the file as it is deleted on uninstall then ...
|
||||
return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
|
||||
}
|
||||
|
||||
// When the user shares a file to another app or opens a file in another app, it is added here.
|
||||
// `HashMap<file, mimetype>` where `file` is the name of the file in the blobdir (not the user-visible filename).
|
||||
// `HashMap<file, mimetype>` where `file` is the name of the file in the blobdir (not the
|
||||
// user-visible filename).
|
||||
public static final HashMap<String, String> sharedFiles = new HashMap<>();
|
||||
|
||||
public static void openForViewOrShare(Context activity, int msg_id, String cmd) {
|
||||
DcContext dcContext = getContext(activity);
|
||||
|
||||
if(!(activity instanceof Activity)) {
|
||||
if (!(activity instanceof Activity)) {
|
||||
// would be nicer to accepting only Activity objects,
|
||||
// however, typically in Android just Context objects are passed around (as this normally does not make a difference).
|
||||
// Accepting only Activity here would force callers to cast, which would easily result in even more ugliness.
|
||||
Toast.makeText(activity, "openForViewOrShare() expects an Activity object", Toast.LENGTH_LONG).show();
|
||||
// however, typically in Android just Context objects are passed around (as this normally does
|
||||
// not make a difference).
|
||||
// Accepting only Activity here would force callers to cast, which would easily result in even
|
||||
// more ugliness.
|
||||
Toast.makeText(activity, "openForViewOrShare() expects an Activity object", Toast.LENGTH_LONG)
|
||||
.show();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -257,7 +270,9 @@ public class DcHelper {
|
||||
try {
|
||||
File file = new File(path);
|
||||
if (!file.exists()) {
|
||||
Toast.makeText(activity, activity.getString(R.string.file_not_found, path), Toast.LENGTH_LONG).show();
|
||||
Toast.makeText(
|
||||
activity, activity.getString(R.string.file_not_found, path), Toast.LENGTH_LONG)
|
||||
.show();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -267,7 +282,14 @@ public class DcHelper {
|
||||
// Build a Uri that will later be passed to AttachmentsContentProvider.openFile().
|
||||
// The last part needs to be `filename`, i.e. the original, user-visible name of the file,
|
||||
// so that the external apps show the name of the file correctly.
|
||||
uri = Uri.parse("content://" + BuildConfig.APPLICATION_ID + ".attachments/" + Uri.encode(file.getName()) + "/" + Uri.encode(filename));
|
||||
uri =
|
||||
Uri.parse(
|
||||
"content://"
|
||||
+ BuildConfig.APPLICATION_ID
|
||||
+ ".attachments/"
|
||||
+ Uri.encode(file.getName())
|
||||
+ "/"
|
||||
+ Uri.encode(filename));
|
||||
|
||||
// As different Android version handle uris in putExtra differently,
|
||||
// we also check on our own that the file was actually shared.
|
||||
@@ -275,7 +297,9 @@ public class DcHelper {
|
||||
sharedFiles.put(file.getName(), mimeType);
|
||||
} else {
|
||||
if (Build.VERSION.SDK_INT >= 24) {
|
||||
uri = FileProvider.getUriForFile(activity, BuildConfig.APPLICATION_ID + ".fileprovider", file);
|
||||
uri =
|
||||
FileProvider.getUriForFile(
|
||||
activity, BuildConfig.APPLICATION_ID + ".fileprovider", file);
|
||||
} else {
|
||||
uri = Uri.fromFile(file);
|
||||
}
|
||||
@@ -292,76 +316,89 @@ public class DcHelper {
|
||||
intent.putExtra(Intent.EXTRA_STREAM, uri);
|
||||
intent.putExtra(Intent.EXTRA_TEXT, msg.getText());
|
||||
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
activity.startActivity(Intent.createChooser(intent, activity.getString(R.string.chat_share_with_title)));
|
||||
activity.startActivity(
|
||||
Intent.createChooser(intent, activity.getString(R.string.chat_share_with_title)));
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
Toast.makeText(activity, String.format("%s (%s)", activity.getString(R.string.no_app_to_handle_data), mimeType), Toast.LENGTH_LONG).show();
|
||||
Toast.makeText(
|
||||
activity,
|
||||
String.format(
|
||||
"%s (%s)", activity.getString(R.string.no_app_to_handle_data), mimeType),
|
||||
Toast.LENGTH_LONG)
|
||||
.show();
|
||||
Log.i(TAG, "opening of external activity failed.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void sendToChat(Context activity, byte[] data, String mimeType, String fileName, String text) {
|
||||
Intent intent = new Intent(activity, ShareActivity.class);
|
||||
intent.setAction(Intent.ACTION_SEND);
|
||||
ShareUtil.setIsFromWebxdc(intent, true);
|
||||
public static void sendToChat(
|
||||
Context activity, byte[] data, String mimeType, String fileName, String text) {
|
||||
Intent intent = new Intent(activity, ShareActivity.class);
|
||||
intent.setAction(Intent.ACTION_SEND);
|
||||
ShareUtil.setIsFromWebxdc(intent, true);
|
||||
|
||||
if (data != null) {
|
||||
Uri uri = PersistentBlobProvider.getInstance().create(activity, data, mimeType, fileName);
|
||||
intent.setType(mimeType);
|
||||
intent.putExtra(Intent.EXTRA_STREAM, uri);
|
||||
ShareUtil.setSharedTitle(intent, activity.getString(R.string.send_file_to, fileName));
|
||||
if (data != null) {
|
||||
Uri uri = PersistentBlobProvider.getInstance().create(activity, data, mimeType, fileName);
|
||||
intent.setType(mimeType);
|
||||
intent.putExtra(Intent.EXTRA_STREAM, uri);
|
||||
ShareUtil.setSharedTitle(intent, activity.getString(R.string.send_file_to, fileName));
|
||||
}
|
||||
|
||||
if (text != null) {
|
||||
intent.putExtra(Intent.EXTRA_TEXT, text);
|
||||
if (data == null) {
|
||||
ShareUtil.setSharedTitle(intent, activity.getString(R.string.send_message_to));
|
||||
}
|
||||
}
|
||||
|
||||
if (text != null) {
|
||||
intent.putExtra(Intent.EXTRA_TEXT, text);
|
||||
if (data == null) {
|
||||
ShareUtil.setSharedTitle(intent, activity.getString(R.string.send_message_to));
|
||||
}
|
||||
}
|
||||
|
||||
activity.startActivity(intent);
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
|
||||
private static void startActivity(Activity activity, Intent intent) {
|
||||
// request for permission to install apks on API 26+ if intent mimetype is an apk
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
|
||||
"application/vnd.android.package-archive".equals(intent.getType()) &&
|
||||
!activity.getPackageManager().canRequestPackageInstalls()) {
|
||||
activity.startActivity(new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).setData(Uri.parse(String.format("package:%s", activity.getPackageName()))));
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
|
||||
&& "application/vnd.android.package-archive".equals(intent.getType())
|
||||
&& !activity.getPackageManager().canRequestPackageInstalls()) {
|
||||
activity.startActivity(
|
||||
new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES)
|
||||
.setData(Uri.parse(String.format("package:%s", activity.getPackageName()))));
|
||||
return;
|
||||
}
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
|
||||
public static String checkMime(String path, String mimeType) {
|
||||
if(mimeType == null || mimeType.equals("application/octet-stream")) {
|
||||
if (mimeType == null || mimeType.equals("application/octet-stream")) {
|
||||
path = path.replaceAll(" ", "");
|
||||
String extension = MediaUtil.getFileExtensionFromUrl(path);
|
||||
String newType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
|
||||
if(newType != null) return newType;
|
||||
if (newType != null) return newType;
|
||||
}
|
||||
return mimeType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the path of a not-yet-existing file in the blobdir with roughly the given filename
|
||||
* and the given extension.
|
||||
* In many cases, since we're using setFileAndDeduplicate now, this wouldn't be necessary anymore
|
||||
* and we could just create a file with a random filename,
|
||||
* but there are a few usages that still need the current behavior (like `openMaps()`).
|
||||
* Return the path of a not-yet-existing file in the blobdir with roughly the given filename and
|
||||
* the given extension. In many cases, since we're using setFileAndDeduplicate now, this wouldn't
|
||||
* be necessary anymore and we could just create a file with a random filename, but there are a
|
||||
* few usages that still need the current behavior (like `openMaps()`).
|
||||
*/
|
||||
public static String getBlobdirFile(DcContext dcContext, String filename, String ext) {
|
||||
filename = FileUtils.sanitizeFilename(filename);
|
||||
ext = FileUtils.sanitizeFilename(ext);
|
||||
String outPath = null;
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
String test = dcContext.getBlobdir() + "/" + filename + (i == 0 ? "" : i < 100 ? "-" + i : "-" + (new Date().getTime() + i)) + ext;
|
||||
String test =
|
||||
dcContext.getBlobdir()
|
||||
+ "/"
|
||||
+ filename
|
||||
+ (i == 0 ? "" : i < 100 ? "-" + i : "-" + (new Date().getTime() + i))
|
||||
+ ext;
|
||||
if (!new File(test).exists()) {
|
||||
outPath = test;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(outPath==null) {
|
||||
if (outPath == null) {
|
||||
// should not happen
|
||||
outPath = dcContext.getBlobdir() + "/" + Math.random();
|
||||
}
|
||||
@@ -369,19 +406,22 @@ public class DcHelper {
|
||||
}
|
||||
|
||||
public static String getBlobdirFile(DcContext dcContext, String path) {
|
||||
String filename = path.substring(path.lastIndexOf('/')+1); // is the whole path if '/' is not found (lastIndexOf() returns -1 then)
|
||||
String filename =
|
||||
path.substring(
|
||||
path.lastIndexOf('/')
|
||||
+ 1); // is the whole path if '/' is not found (lastIndexOf() returns -1 then)
|
||||
String ext = "";
|
||||
int point = filename.indexOf('.');
|
||||
if(point!=-1) {
|
||||
if (point != -1) {
|
||||
ext = filename.substring(point);
|
||||
filename = filename.substring(0, point);
|
||||
}
|
||||
return getBlobdirFile(dcContext, filename, ext);
|
||||
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static ThreadRecord getThreadRecord(Context context, DcLot summary, DcChat chat) { // adapted from ThreadDatabase.getCurrent()
|
||||
public static ThreadRecord getThreadRecord(
|
||||
Context context, DcLot summary, DcChat chat) { // adapted from ThreadDatabase.getCurrent()
|
||||
int chatId = chat.getId();
|
||||
|
||||
String body = summary.getText1();
|
||||
@@ -394,14 +434,23 @@ public class DcHelper {
|
||||
long date = summary.getTimestamp();
|
||||
int unreadCount = getContext(context).getFreshMsgCount(chatId);
|
||||
|
||||
return new ThreadRecord(body, recipient, date,
|
||||
unreadCount, chatId,
|
||||
chat.getVisibility(), chat.isSendingLocations(), chat.isMuted(), chat.isContactRequest(), summary);
|
||||
return new ThreadRecord(
|
||||
body,
|
||||
recipient,
|
||||
date,
|
||||
unreadCount,
|
||||
chatId,
|
||||
chat.getVisibility(),
|
||||
chat.isSendingLocations(),
|
||||
chat.isMuted(),
|
||||
chat.isContactRequest(),
|
||||
summary);
|
||||
}
|
||||
|
||||
public static boolean isNetworkConnected(Context context) {
|
||||
try {
|
||||
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
ConnectivityManager cm =
|
||||
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
NetworkInfo netInfo = cm.getActiveNetworkInfo();
|
||||
if (netInfo != null && netInfo.isConnected()) {
|
||||
return true;
|
||||
@@ -414,105 +463,110 @@ public class DcHelper {
|
||||
|
||||
/**
|
||||
* Gets a string you can show to the user with basic information about connectivity.
|
||||
*
|
||||
* @param context
|
||||
* @param connectedString Usually "Connected", but when using this as the title in
|
||||
* ConversationListActivity, we want to write "Delta Chat"
|
||||
* or the user's display name there instead.
|
||||
* ConversationListActivity, we want to write "Delta Chat" or the user's display name there
|
||||
* instead.
|
||||
* @return
|
||||
*/
|
||||
public static String getConnectivitySummary(Context context, String connectedString) {
|
||||
int connectivity = getContext(context).getConnectivity();
|
||||
if (connectivity >= DcContext.DC_CONNECTIVITY_CONNECTED) {
|
||||
return connectedString;
|
||||
} else if (connectivity >= DcContext.DC_CONNECTIVITY_WORKING) {
|
||||
return context.getString(R.string.connectivity_updating);
|
||||
} else if (connectivity >= DcContext.DC_CONNECTIVITY_CONNECTING) {
|
||||
return context.getString(R.string.connectivity_connecting);
|
||||
} else {
|
||||
return context.getString(R.string.connectivity_not_connected);
|
||||
}
|
||||
int connectivity = getContext(context).getConnectivity();
|
||||
if (connectivity >= DcContext.DC_CONNECTIVITY_CONNECTED) {
|
||||
return connectedString;
|
||||
} else if (connectivity >= DcContext.DC_CONNECTIVITY_WORKING) {
|
||||
return context.getString(R.string.connectivity_updating);
|
||||
} else if (connectivity >= DcContext.DC_CONNECTIVITY_CONNECTING) {
|
||||
return context.getString(R.string.connectivity_connecting);
|
||||
} else {
|
||||
return context.getString(R.string.connectivity_not_connected);
|
||||
}
|
||||
}
|
||||
|
||||
public static void showProtectionEnabledDialog(Context context) {
|
||||
new AlertDialog.Builder(context)
|
||||
.setMessage(context.getString(R.string.chat_protection_enabled_explanation))
|
||||
.setNeutralButton(R.string.learn_more, (d, w) -> openHelp(context, "#e2ee"))
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.setCancelable(true)
|
||||
.show();
|
||||
.setMessage(context.getString(R.string.chat_protection_enabled_explanation))
|
||||
.setNeutralButton(R.string.learn_more, (d, w) -> openHelp(context, "#e2ee"))
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.setCancelable(true)
|
||||
.show();
|
||||
}
|
||||
|
||||
public static void showInvalidUnencryptedDialog(Context context) {
|
||||
new AlertDialog.Builder(context)
|
||||
.setMessage(context.getString(R.string.invalid_unencrypted_explanation))
|
||||
.setNeutralButton(R.string.learn_more, (d, w) -> openHelp(context, "#howtoe2ee"))
|
||||
.setNegativeButton(R.string.qrscan_title, (d, w) -> context.startActivity(new Intent(context, QrActivity.class)))
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.setCancelable(true)
|
||||
.show();
|
||||
.setMessage(context.getString(R.string.invalid_unencrypted_explanation))
|
||||
.setNeutralButton(R.string.learn_more, (d, w) -> openHelp(context, "#howtoe2ee"))
|
||||
.setNegativeButton(
|
||||
R.string.qrscan_title,
|
||||
(d, w) -> context.startActivity(new Intent(context, QrActivity.class)))
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.setCancelable(true)
|
||||
.show();
|
||||
}
|
||||
|
||||
public static void openHelp(Context context, String section) {
|
||||
Intent intent = new Intent(context, LocalHelpActivity.class);
|
||||
if (section != null) { intent.putExtra(LocalHelpActivity.SECTION_EXTRA, section); }
|
||||
if (section != null) {
|
||||
intent.putExtra(LocalHelpActivity.SECTION_EXTRA, section);
|
||||
}
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* For the PGP-Contacts migration, things can go wrong.
|
||||
* The migration happens when the account is setup, at which point no events can be sent yet.
|
||||
* So, instead, if something goes wrong, it's returned by getLastError().
|
||||
* This function shows the error message to the user.
|
||||
* <p>
|
||||
* A few releases after the PGP-contacts migration (which happened in 2025-05),
|
||||
* we can remove this function again.
|
||||
*/
|
||||
public static void maybeShowMigrationError(Context context) {
|
||||
try {
|
||||
String lastError = DcHelper.getRpc(context).getMigrationError(DcHelper.getContext(context).getAccountId());
|
||||
/**
|
||||
* For the PGP-Contacts migration, things can go wrong. The migration happens when the account is
|
||||
* setup, at which point no events can be sent yet. So, instead, if something goes wrong, it's
|
||||
* returned by getLastError(). This function shows the error message to the user.
|
||||
*
|
||||
* <p>A few releases after the PGP-contacts migration (which happened in 2025-05), we can remove
|
||||
* this function again.
|
||||
*/
|
||||
public static void maybeShowMigrationError(Context context) {
|
||||
try {
|
||||
String lastError =
|
||||
DcHelper.getRpc(context).getMigrationError(DcHelper.getContext(context).getAccountId());
|
||||
|
||||
if (lastError != null && !lastError.isEmpty()) {
|
||||
Log.w(TAG, "Opening account failed, trying to share error: " + lastError);
|
||||
if (lastError != null && !lastError.isEmpty()) {
|
||||
Log.w(TAG, "Opening account failed, trying to share error: " + lastError);
|
||||
|
||||
String subject = "Delta Chat failed to update";
|
||||
String email = "delta@merlinux.eu";
|
||||
String subject = "Delta Chat failed to update";
|
||||
String email = "delta@merlinux.eu";
|
||||
|
||||
new AlertDialog.Builder(context)
|
||||
.setMessage(context.getString(R.string.error_x, lastError))
|
||||
.setNeutralButton(R.string.global_menu_edit_copy_desktop, (d, which) -> {
|
||||
Util.writeTextToClipboard(context, lastError);
|
||||
})
|
||||
.setPositiveButton(R.string.menu_send, (d, which) -> {
|
||||
Intent sharingIntent = new Intent(
|
||||
Intent.ACTION_SENDTO, Uri.fromParts(
|
||||
"mailto", email, null
|
||||
)
|
||||
);
|
||||
sharingIntent.putExtra(Intent.EXTRA_EMAIL, new String[]{email});
|
||||
sharingIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
|
||||
sharingIntent.putExtra(Intent.EXTRA_TEXT, lastError);
|
||||
new AlertDialog.Builder(context)
|
||||
.setMessage(context.getString(R.string.error_x, lastError))
|
||||
.setNeutralButton(
|
||||
R.string.global_menu_edit_copy_desktop,
|
||||
(d, which) -> {
|
||||
Util.writeTextToClipboard(context, lastError);
|
||||
})
|
||||
.setPositiveButton(
|
||||
R.string.menu_send,
|
||||
(d, which) -> {
|
||||
Intent sharingIntent =
|
||||
new Intent(Intent.ACTION_SENDTO, Uri.fromParts("mailto", email, null));
|
||||
sharingIntent.putExtra(Intent.EXTRA_EMAIL, new String[] {email});
|
||||
sharingIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
|
||||
sharingIntent.putExtra(Intent.EXTRA_TEXT, lastError);
|
||||
|
||||
if (sharingIntent.resolveActivity(context.getPackageManager()) == null) {
|
||||
Log.w(TAG, "No email client found to send crash report");
|
||||
sharingIntent = new Intent(Intent.ACTION_SEND);
|
||||
sharingIntent.setType("text/plain");
|
||||
sharingIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
|
||||
sharingIntent.putExtra(Intent.EXTRA_TEXT, lastError);
|
||||
sharingIntent.putExtra(Intent.EXTRA_EMAIL, email);
|
||||
}
|
||||
if (sharingIntent.resolveActivity(context.getPackageManager()) == null) {
|
||||
Log.w(TAG, "No email client found to send crash report");
|
||||
sharingIntent = new Intent(Intent.ACTION_SEND);
|
||||
sharingIntent.setType("text/plain");
|
||||
sharingIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
|
||||
sharingIntent.putExtra(Intent.EXTRA_TEXT, lastError);
|
||||
sharingIntent.putExtra(Intent.EXTRA_EMAIL, email);
|
||||
}
|
||||
|
||||
Intent chooser =
|
||||
Intent.createChooser(sharingIntent, "Send using...");
|
||||
chooser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
chooser.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
|
||||
Intent chooser = Intent.createChooser(sharingIntent, "Send using...");
|
||||
chooser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
chooser.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
|
||||
|
||||
context.startActivity(chooser);
|
||||
})
|
||||
.setCancelable(false)
|
||||
.show();
|
||||
}
|
||||
} catch (RpcException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
context.startActivity(chooser);
|
||||
})
|
||||
.setCancelable(false)
|
||||
.show();
|
||||
}
|
||||
} catch (RpcException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,71 @@ public class PersistentBlobProvider {
|
||||
|
||||
private PersistentBlobProvider() {}
|
||||
|
||||
public Uri create(
|
||||
@NonNull Context context,
|
||||
@NonNull File inputFile,
|
||||
@NonNull String mimeType,
|
||||
@Nullable String fileName,
|
||||
@Nullable Long fileSize)
|
||||
throws IOException {
|
||||
|
||||
if (!inputFile.exists()) {
|
||||
throw new IOException("Input file does not exist: " + inputFile.getAbsolutePath());
|
||||
}
|
||||
|
||||
final long id = System.currentTimeMillis();
|
||||
|
||||
if (fileName == null) {
|
||||
fileName = "file." + getExtensionFromMimeType(mimeType);
|
||||
}
|
||||
|
||||
if (fileSize == null) {
|
||||
fileSize = inputFile.length();
|
||||
}
|
||||
|
||||
File destFile = getModernCacheFile(context, id);
|
||||
|
||||
// Try move first
|
||||
boolean moved = inputFile.renameTo(destFile);
|
||||
|
||||
if (!moved) {
|
||||
// Move failed, copy instead
|
||||
InputStream input = null;
|
||||
OutputStream output = null;
|
||||
try {
|
||||
input = new FileInputStream(inputFile);
|
||||
output = new FileOutputStream(destFile);
|
||||
Util.copy(input, output);
|
||||
} finally {
|
||||
if (output != null) {
|
||||
try {
|
||||
output.close();
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
if (input != null) {
|
||||
try {
|
||||
input.close();
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final Uri uniqueUri =
|
||||
CONTENT_URI
|
||||
.buildUpon()
|
||||
.appendPath(mimeType)
|
||||
.appendPath(fileName)
|
||||
.appendEncodedPath(String.valueOf(fileSize))
|
||||
.appendEncodedPath(String.valueOf(System.currentTimeMillis()))
|
||||
.build();
|
||||
|
||||
return ContentUris.withAppendedId(uniqueUri, id);
|
||||
}
|
||||
|
||||
public Uri create(
|
||||
@NonNull Context context,
|
||||
@NonNull byte[] blobBytes,
|
||||
|
||||
@@ -41,6 +41,7 @@ public class MediaUtil {
|
||||
public static final String IMAGE_JPEG = "image/jpeg";
|
||||
public static final String IMAGE_GIF = "image/gif";
|
||||
public static final String AUDIO_AAC = "audio/aac";
|
||||
public static final String AUDIO_M4A = "audio/mp4";
|
||||
public static final String AUDIO_UNSPECIFIED = "audio/*";
|
||||
public static final String VIDEO_UNSPECIFIED = "video/*";
|
||||
public static final String OCTET = "application/octet-stream";
|
||||
@@ -294,6 +295,8 @@ public class MediaUtil {
|
||||
switch (contentType) {
|
||||
case AUDIO_AAC:
|
||||
return "aac";
|
||||
case AUDIO_M4A:
|
||||
return "m4a";
|
||||
case IMAGE_WEBP:
|
||||
return "webp";
|
||||
case WEBXDC:
|
||||
|
||||
Reference in New Issue
Block a user