mirror of
https://github.com/oxen-io/session-android.git
synced 2025-12-03 10:52:21 +00:00
Support for receiving arbitrary attachment types
// FREEBIE
This commit is contained in:
120
src/org/thoughtcrime/securesms/util/LimitedInputStream.java
Normal file
120
src/org/thoughtcrime/securesms/util/LimitedInputStream.java
Normal file
@@ -0,0 +1,120 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
|
||||
/**
|
||||
* An input stream, which limits its data size. This stream is
|
||||
* used, if the content length is unknown.
|
||||
*/
|
||||
public class LimitedInputStream extends FilterInputStream {
|
||||
|
||||
/**
|
||||
* The maximum size of an item, in bytes.
|
||||
*/
|
||||
private long sizeMax;
|
||||
|
||||
/**
|
||||
* The current number of bytes.
|
||||
*/
|
||||
private long count;
|
||||
|
||||
/**
|
||||
* Whether this stream is already closed.
|
||||
*/
|
||||
private boolean closed;
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param pIn The input stream, which shall be limited.
|
||||
* @param pSizeMax The limit; no more than this number of bytes
|
||||
* shall be returned by the source stream.
|
||||
*/
|
||||
public LimitedInputStream(InputStream pIn, long pSizeMax) {
|
||||
super(pIn);
|
||||
sizeMax = pSizeMax;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the next byte of data from this input stream. The value
|
||||
* byte is returned as an <code>int</code> in the range
|
||||
* <code>0</code> to <code>255</code>. If no byte is available
|
||||
* because the end of the stream has been reached, the value
|
||||
* <code>-1</code> is returned. This method blocks until input data
|
||||
* is available, the end of the stream is detected, or an exception
|
||||
* is thrown.
|
||||
*
|
||||
* This method
|
||||
* simply performs <code>in.read()</code> and returns the result.
|
||||
*
|
||||
* @return the next byte of data, or <code>-1</code> if the end of the
|
||||
* stream is reached.
|
||||
* @exception IOException if an I/O error occurs.
|
||||
* @see java.io.FilterInputStream#in
|
||||
*/
|
||||
public int read() throws IOException {
|
||||
if (count >= sizeMax) return -1;
|
||||
|
||||
int res = super.read();
|
||||
if (res != -1) {
|
||||
count++;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads up to <code>len</code> bytes of data from this input stream
|
||||
* into an array of bytes. If <code>len</code> is not zero, the method
|
||||
* blocks until some input is available; otherwise, no
|
||||
* bytes are read and <code>0</code> is returned.
|
||||
*
|
||||
* This method simply performs <code>in.read(b, off, len)</code>
|
||||
* and returns the result.
|
||||
*
|
||||
* @param b the buffer into which the data is read.
|
||||
* @param off The start offset in the destination array
|
||||
* <code>b</code>.
|
||||
* @param len the maximum number of bytes read.
|
||||
* @return the total number of bytes read into the buffer, or
|
||||
* <code>-1</code> if there is no more data because the end of
|
||||
* the stream has been reached.
|
||||
* @exception NullPointerException If <code>b</code> is <code>null</code>.
|
||||
* @exception IndexOutOfBoundsException If <code>off</code> is negative,
|
||||
* <code>len</code> is negative, or <code>len</code> is greater than
|
||||
* <code>b.length - off</code>
|
||||
* @exception IOException if an I/O error occurs.
|
||||
* @see java.io.FilterInputStream#in
|
||||
*/
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
if (count >= sizeMax) return -1;
|
||||
|
||||
long correctLength = Math.min(len, sizeMax - count);
|
||||
|
||||
int res = super.read(b, off, Util.toIntExact(correctLength));
|
||||
if (res > 0) {
|
||||
count += res;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.mms.AudioSlide;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
import org.thoughtcrime.securesms.mms.DocumentSlide;
|
||||
import org.thoughtcrime.securesms.mms.GifSlide;
|
||||
import org.thoughtcrime.securesms.mms.ImageSlide;
|
||||
import org.thoughtcrime.securesms.mms.MmsSlide;
|
||||
@@ -82,6 +83,8 @@ public class MediaUtil {
|
||||
slide = new AudioSlide(context, attachment);
|
||||
} else if (isMms(attachment.getContentType())) {
|
||||
slide = new MmsSlide(context, attachment);
|
||||
} else {
|
||||
slide = new DocumentSlide(context, attachment);
|
||||
}
|
||||
|
||||
return slide;
|
||||
|
||||
41
src/org/thoughtcrime/securesms/util/MemoryFileUtil.java
Normal file
41
src/org/thoughtcrime/securesms/util/MemoryFileUtil.java
Normal file
@@ -0,0 +1,41 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.MemoryFile;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public class MemoryFileUtil {
|
||||
|
||||
public static ParcelFileDescriptor getParcelFileDescriptor(MemoryFile file) throws IOException {
|
||||
try {
|
||||
Method method = MemoryFile.class.getDeclaredMethod("getFileDescriptor");
|
||||
FileDescriptor fileDescriptor = (FileDescriptor) method.invoke(file);
|
||||
|
||||
Field field = fileDescriptor.getClass().getDeclaredField("descriptor");
|
||||
field.setAccessible(true);
|
||||
|
||||
int fd = field.getInt(fileDescriptor);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 13) {
|
||||
return ParcelFileDescriptor.adoptFd(fd);
|
||||
} else {
|
||||
return ParcelFileDescriptor.dup(fileDescriptor);
|
||||
}
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new IOException(e);
|
||||
} catch (InvocationTargetException e) {
|
||||
throw new IOException(e);
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new IOException(e);
|
||||
} catch (NoSuchFieldException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,16 @@ package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface.OnClickListener;
|
||||
import android.content.Intent;
|
||||
import android.media.MediaScannerConnection;
|
||||
import android.net.Uri;
|
||||
import android.os.Environment;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.design.widget.Snackbar;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.webkit.MimeTypeMap;
|
||||
import android.widget.Toast;
|
||||
|
||||
@@ -15,6 +19,7 @@ import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
@@ -24,69 +29,76 @@ import java.io.OutputStream;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.text.SimpleDateFormat;
|
||||
|
||||
public class SaveAttachmentTask extends ProgressDialogAsyncTask<SaveAttachmentTask.Attachment, Void, Integer> {
|
||||
public class SaveAttachmentTask extends ProgressDialogAsyncTask<SaveAttachmentTask.Attachment, Void, Pair<Integer, File>> {
|
||||
private static final String TAG = SaveAttachmentTask.class.getSimpleName();
|
||||
|
||||
private static final int SUCCESS = 0;
|
||||
private static final int FAILURE = 1;
|
||||
private static final int WRITE_ACCESS_FAILURE = 2;
|
||||
|
||||
private final WeakReference<Context> contextReference;
|
||||
private final WeakReference<Context> contextReference;
|
||||
private final WeakReference<MasterSecret> masterSecretReference;
|
||||
private final WeakReference<View> view;
|
||||
|
||||
private final int attachmentCount;
|
||||
|
||||
public SaveAttachmentTask(Context context, MasterSecret masterSecret) {
|
||||
this(context, masterSecret, 1);
|
||||
public SaveAttachmentTask(Context context, MasterSecret masterSecret, View view) {
|
||||
this(context, masterSecret, view, 1);
|
||||
}
|
||||
|
||||
public SaveAttachmentTask(Context context, MasterSecret masterSecret, int count) {
|
||||
public SaveAttachmentTask(Context context, MasterSecret masterSecret, View view, int count) {
|
||||
super(context,
|
||||
context.getResources().getQuantityString(R.plurals.ConversationFragment_saving_n_attachments, count, count),
|
||||
context.getResources().getQuantityString(R.plurals.ConversationFragment_saving_n_attachments_to_sd_card, count, count));
|
||||
this.contextReference = new WeakReference<>(context);
|
||||
this.masterSecretReference = new WeakReference<>(masterSecret);
|
||||
this.view = new WeakReference<>(view);
|
||||
this.attachmentCount = count;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Integer doInBackground(SaveAttachmentTask.Attachment... attachments) {
|
||||
protected Pair<Integer, File> doInBackground(SaveAttachmentTask.Attachment... attachments) {
|
||||
if (attachments == null || attachments.length == 0) {
|
||||
throw new AssertionError("must pass in at least one attachment");
|
||||
}
|
||||
|
||||
try {
|
||||
Context context = contextReference.get();
|
||||
Context context = contextReference.get();
|
||||
MasterSecret masterSecret = masterSecretReference.get();
|
||||
File directory = null;
|
||||
|
||||
if (!Environment.getExternalStorageDirectory().canWrite()) {
|
||||
return WRITE_ACCESS_FAILURE;
|
||||
return new Pair<>(WRITE_ACCESS_FAILURE, null);
|
||||
}
|
||||
|
||||
if (context == null) {
|
||||
return FAILURE;
|
||||
return new Pair<>(FAILURE, null);
|
||||
}
|
||||
|
||||
for (Attachment attachment : attachments) {
|
||||
if (attachment != null && !saveAttachment(context, masterSecret, attachment)) {
|
||||
return FAILURE;
|
||||
if (attachment != null) {
|
||||
directory = saveAttachment(context, masterSecret, attachment);
|
||||
if (directory == null) return new Pair<>(FAILURE, null);
|
||||
}
|
||||
}
|
||||
|
||||
return SUCCESS;
|
||||
if (attachments.length > 1) return new Pair<>(SUCCESS, null);
|
||||
else return new Pair<>(SUCCESS, directory);
|
||||
} catch (IOException ioe) {
|
||||
Log.w(TAG, ioe);
|
||||
return FAILURE;
|
||||
return new Pair<>(FAILURE, null);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean saveAttachment(Context context, MasterSecret masterSecret, Attachment attachment) throws IOException {
|
||||
String contentType = MediaUtil.getCorrectedMimeType(attachment.contentType);
|
||||
File mediaFile = constructOutputFile(contentType, attachment.date);
|
||||
private @Nullable File saveAttachment(Context context, MasterSecret masterSecret, Attachment attachment)
|
||||
throws IOException
|
||||
{
|
||||
String contentType = MediaUtil.getCorrectedMimeType(attachment.contentType);
|
||||
File mediaFile = constructOutputFile(attachment.fileName, contentType, attachment.date);
|
||||
InputStream inputStream = PartAuthority.getAttachmentStream(context, masterSecret, attachment.uri);
|
||||
|
||||
if (inputStream == null) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
OutputStream outputStream = new FileOutputStream(mediaFile);
|
||||
@@ -95,16 +107,16 @@ public class SaveAttachmentTask extends ProgressDialogAsyncTask<SaveAttachmentTa
|
||||
MediaScannerConnection.scanFile(context, new String[]{mediaFile.getAbsolutePath()},
|
||||
new String[]{contentType}, null);
|
||||
|
||||
return true;
|
||||
return mediaFile.getParentFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Integer result) {
|
||||
protected void onPostExecute(final Pair<Integer, File> result) {
|
||||
super.onPostExecute(result);
|
||||
Context context = contextReference.get();
|
||||
final Context context = contextReference.get();
|
||||
if (context == null) return;
|
||||
|
||||
switch (result) {
|
||||
switch (result.first()) {
|
||||
case FAILURE:
|
||||
Toast.makeText(context,
|
||||
context.getResources().getQuantityText(R.plurals.ConversationFragment_error_while_saving_attachments_to_sd_card,
|
||||
@@ -112,10 +124,26 @@ public class SaveAttachmentTask extends ProgressDialogAsyncTask<SaveAttachmentTa
|
||||
Toast.LENGTH_LONG).show();
|
||||
break;
|
||||
case SUCCESS:
|
||||
Toast.makeText(context,
|
||||
context.getResources().getQuantityText(R.plurals.ConversationFragment_files_saved_successfully,
|
||||
attachmentCount),
|
||||
Toast.LENGTH_LONG).show();
|
||||
Snackbar snackbar = Snackbar.make(view.get(),
|
||||
context.getResources().getQuantityText(R.plurals.ConversationFragment_files_saved_successfully, attachmentCount),
|
||||
Snackbar.LENGTH_SHORT);
|
||||
|
||||
if (result.second() != null) {
|
||||
snackbar.setDuration(Snackbar.LENGTH_LONG);
|
||||
snackbar.setAction(R.string.SaveAttachmentTask_open_directory, new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.setDataAndType(Uri.fromFile(result.second()), "resource/folder");
|
||||
if (intent.resolveActivityInfo(context.getPackageManager(), 0) != null)
|
||||
{
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
snackbar.show();
|
||||
break;
|
||||
case WRITE_ACCESS_FAILURE:
|
||||
Toast.makeText(context, R.string.ConversationFragment_unable_to_write_to_sd_card_exclamation,
|
||||
@@ -124,7 +152,9 @@ public class SaveAttachmentTask extends ProgressDialogAsyncTask<SaveAttachmentTa
|
||||
}
|
||||
}
|
||||
|
||||
private File constructOutputFile(String contentType, long timestamp) throws IOException {
|
||||
private File constructOutputFile(@Nullable String fileName, String contentType, long timestamp)
|
||||
throws IOException
|
||||
{
|
||||
File sdCard = Environment.getExternalStorageDirectory();
|
||||
File outputDirectory;
|
||||
|
||||
@@ -140,32 +170,54 @@ public class SaveAttachmentTask extends ProgressDialogAsyncTask<SaveAttachmentTa
|
||||
|
||||
if (!outputDirectory.mkdirs()) Log.w(TAG, "mkdirs() returned false, attempting to continue");
|
||||
|
||||
MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
|
||||
String extension = mimeTypeMap.getExtensionFromMimeType(contentType);
|
||||
SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd-HHmmss");
|
||||
String base = "signal-" + dateFormatter.format(timestamp);
|
||||
if (fileName == null) {
|
||||
MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
|
||||
String extension = mimeTypeMap.getExtensionFromMimeType(contentType);
|
||||
SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd-HHmmss");
|
||||
String base = "signal-" + dateFormatter.format(timestamp);
|
||||
|
||||
if (extension == null) extension = "attach";
|
||||
if (extension == null) extension = "attach";
|
||||
|
||||
fileName = base + "." + extension;
|
||||
}
|
||||
|
||||
int i = 0;
|
||||
File file = new File(outputDirectory, fileName);
|
||||
|
||||
int i = 0;
|
||||
File file = new File(outputDirectory, base + "." + extension);
|
||||
while (file.exists()) {
|
||||
file = new File(outputDirectory, base + "-" + (++i) + "." + extension);
|
||||
String[] fileParts = getFileNameParts(fileName);
|
||||
file = new File(outputDirectory, fileParts[0] + "-" + (++i) + "." + fileParts[1]);
|
||||
}
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
private String[] getFileNameParts(String fileName) {
|
||||
String[] result = new String[2];
|
||||
String[] tokens = fileName.split("\\.(?=[^\\.]+$)");
|
||||
|
||||
result[0] = tokens[0];
|
||||
|
||||
if (tokens.length > 1) result[1] = tokens[1];
|
||||
else result[1] = "";
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static class Attachment {
|
||||
public Uri uri;
|
||||
public String fileName;
|
||||
public String contentType;
|
||||
public long date;
|
||||
|
||||
public Attachment(@NonNull Uri uri, @NonNull String contentType, long date) {
|
||||
public Attachment(@NonNull Uri uri, @NonNull String contentType,
|
||||
long date, @Nullable String fileName)
|
||||
{
|
||||
if (uri == null || contentType == null || date < 0) {
|
||||
throw new AssertionError("uri, content type, and date must all be specified");
|
||||
}
|
||||
this.uri = uri;
|
||||
this.fileName = fileName;
|
||||
this.contentType = contentType;
|
||||
this.date = date;
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ import java.io.OutputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
@@ -164,6 +165,14 @@ public class Util {
|
||||
}
|
||||
}
|
||||
|
||||
public static void close(InputStream in) {
|
||||
try {
|
||||
in.close();
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void close(OutputStream out) {
|
||||
try {
|
||||
out.close();
|
||||
@@ -172,6 +181,19 @@ public class Util {
|
||||
}
|
||||
}
|
||||
|
||||
public static long getStreamLength(InputStream in) throws IOException {
|
||||
byte[] buffer = new byte[4096];
|
||||
int totalSize = 0;
|
||||
|
||||
int read;
|
||||
|
||||
while ((read = in.read(buffer)) != -1) {
|
||||
totalSize += read;
|
||||
}
|
||||
|
||||
return totalSize;
|
||||
}
|
||||
|
||||
public static String canonicalizeNumber(Context context, String number)
|
||||
throws InvalidNumberException
|
||||
{
|
||||
@@ -463,4 +485,13 @@ public class Util {
|
||||
public static boolean isEquals(@Nullable Long first, long second) {
|
||||
return first != null && first == second;
|
||||
}
|
||||
|
||||
public static String getPrettyFileSize(long sizeBytes) {
|
||||
if (sizeBytes <= 0) return "0";
|
||||
|
||||
String[] units = new String[]{"B", "kB", "MB", "GB", "TB"};
|
||||
int digitGroups = (int) (Math.log10(sizeBytes) / Math.log10(1024));
|
||||
|
||||
return new DecimalFormat("#,##0.#").format(sizeBytes/Math.pow(1024, digitGroups)) + " " + units[digitGroups];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user