basic arbitrary file transfer
This commit is contained in:
parent
4c504dea7a
commit
7a90ca429b
|
@ -2,7 +2,7 @@ package eu.siacs.conversations.entities;
|
||||||
|
|
||||||
public interface Downloadable {
|
public interface Downloadable {
|
||||||
|
|
||||||
public final String[] VALID_EXTENSIONS = {"webp", "jpeg", "jpg", "png", "jpe"};
|
public final String[] VALID_IMAGE_EXTENSIONS = {"webp", "jpeg", "jpg", "png", "jpe"};
|
||||||
public final String[] VALID_CRYPTO_EXTENSIONS = {"pgp", "gpg", "otr"};
|
public final String[] VALID_CRYPTO_EXTENSIONS = {"pgp", "gpg", "otr"};
|
||||||
|
|
||||||
public static final int STATUS_UNKNOWN = 0x200;
|
public static final int STATUS_UNKNOWN = 0x200;
|
||||||
|
@ -18,4 +18,8 @@ public interface Downloadable {
|
||||||
public int getStatus();
|
public int getStatus();
|
||||||
|
|
||||||
public long getFileSize();
|
public long getFileSize();
|
||||||
|
|
||||||
|
public int getProgress();
|
||||||
|
|
||||||
|
public String getMimeType();
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import java.io.FileNotFoundException;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
import java.net.URLConnection;
|
||||||
import java.security.InvalidAlgorithmParameterException;
|
import java.security.InvalidAlgorithmParameterException;
|
||||||
import java.security.InvalidKeyException;
|
import java.security.InvalidKeyException;
|
||||||
import java.security.Key;
|
import java.security.Key;
|
||||||
|
@ -28,6 +29,7 @@ public class DownloadableFile extends File {
|
||||||
private long expectedSize = 0;
|
private long expectedSize = 0;
|
||||||
private String sha1sum;
|
private String sha1sum;
|
||||||
private Key aeskey;
|
private Key aeskey;
|
||||||
|
private String mime;
|
||||||
|
|
||||||
private byte[] iv = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
|
private byte[] iv = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
|
||||||
0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0xf };
|
0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0xf };
|
||||||
|
@ -52,6 +54,16 @@ public class DownloadableFile extends File {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getMimeType() {
|
||||||
|
if (mime==null) {
|
||||||
|
mime = URLConnection.guessContentTypeFromName(this.getAbsolutePath());
|
||||||
|
if (mime == null) {
|
||||||
|
mime = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mime;
|
||||||
|
}
|
||||||
|
|
||||||
public void setExpectedSize(long size) {
|
public void setExpectedSize(long size) {
|
||||||
this.expectedSize = size;
|
this.expectedSize = size;
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ public class Message extends AbstractEntity {
|
||||||
|
|
||||||
public static final int TYPE_TEXT = 0;
|
public static final int TYPE_TEXT = 0;
|
||||||
public static final int TYPE_IMAGE = 1;
|
public static final int TYPE_IMAGE = 1;
|
||||||
public static final int TYPE_AUDIO = 2;
|
public static final int TYPE_FILE = 2;
|
||||||
public static final int TYPE_STATUS = 3;
|
public static final int TYPE_STATUS = 3;
|
||||||
public static final int TYPE_PRIVATE = 4;
|
public static final int TYPE_PRIVATE = 4;
|
||||||
|
|
||||||
|
@ -45,6 +45,7 @@ public class Message extends AbstractEntity {
|
||||||
public static String STATUS = "status";
|
public static String STATUS = "status";
|
||||||
public static String TYPE = "type";
|
public static String TYPE = "type";
|
||||||
public static String REMOTE_MSG_ID = "remoteMsgId";
|
public static String REMOTE_MSG_ID = "remoteMsgId";
|
||||||
|
public static String RELATIVE_FILE_PATH = "relativeFilePath";
|
||||||
public boolean markable = false;
|
public boolean markable = false;
|
||||||
protected String conversationUuid;
|
protected String conversationUuid;
|
||||||
protected Jid counterpart;
|
protected Jid counterpart;
|
||||||
|
@ -55,6 +56,7 @@ public class Message extends AbstractEntity {
|
||||||
protected int encryption;
|
protected int encryption;
|
||||||
protected int status;
|
protected int status;
|
||||||
protected int type;
|
protected int type;
|
||||||
|
protected String relativeFilePath;
|
||||||
protected boolean read = true;
|
protected boolean read = true;
|
||||||
protected String remoteMsgId = null;
|
protected String remoteMsgId = null;
|
||||||
protected Conversation conversation = null;
|
protected Conversation conversation = null;
|
||||||
|
@ -74,13 +76,13 @@ public class Message extends AbstractEntity {
|
||||||
this(java.util.UUID.randomUUID().toString(), conversation.getUuid(),
|
this(java.util.UUID.randomUUID().toString(), conversation.getUuid(),
|
||||||
conversation.getContactJid().toBareJid(), null, body, System
|
conversation.getContactJid().toBareJid(), null, body, System
|
||||||
.currentTimeMillis(), encryption,
|
.currentTimeMillis(), encryption,
|
||||||
status, TYPE_TEXT, null);
|
status, TYPE_TEXT, null,null);
|
||||||
this.conversation = conversation;
|
this.conversation = conversation;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Message(final String uuid, final String conversationUUid, final Jid counterpart,
|
public Message(final String uuid, final String conversationUUid, final Jid counterpart,
|
||||||
final String trueCounterpart, final String body, final long timeSent,
|
final String trueCounterpart, final String body, final long timeSent,
|
||||||
final int encryption, final int status, final int type, final String remoteMsgId) {
|
final int encryption, final int status, final int type, final String remoteMsgId, final String relativeFilePath) {
|
||||||
this.uuid = uuid;
|
this.uuid = uuid;
|
||||||
this.conversationUuid = conversationUUid;
|
this.conversationUuid = conversationUUid;
|
||||||
this.counterpart = counterpart;
|
this.counterpart = counterpart;
|
||||||
|
@ -91,6 +93,7 @@ public class Message extends AbstractEntity {
|
||||||
this.status = status;
|
this.status = status;
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.remoteMsgId = remoteMsgId;
|
this.remoteMsgId = remoteMsgId;
|
||||||
|
this.relativeFilePath = relativeFilePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Message fromCursor(Cursor cursor) {
|
public static Message fromCursor(Cursor cursor) {
|
||||||
|
@ -114,7 +117,8 @@ public class Message extends AbstractEntity {
|
||||||
cursor.getInt(cursor.getColumnIndex(ENCRYPTION)),
|
cursor.getInt(cursor.getColumnIndex(ENCRYPTION)),
|
||||||
cursor.getInt(cursor.getColumnIndex(STATUS)),
|
cursor.getInt(cursor.getColumnIndex(STATUS)),
|
||||||
cursor.getInt(cursor.getColumnIndex(TYPE)),
|
cursor.getInt(cursor.getColumnIndex(TYPE)),
|
||||||
cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)));
|
cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)),
|
||||||
|
cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Message createStatusMessage(Conversation conversation) {
|
public static Message createStatusMessage(Conversation conversation) {
|
||||||
|
@ -141,6 +145,7 @@ public class Message extends AbstractEntity {
|
||||||
values.put(STATUS, status);
|
values.put(STATUS, status);
|
||||||
values.put(TYPE, type);
|
values.put(TYPE, type);
|
||||||
values.put(REMOTE_MSG_ID, remoteMsgId);
|
values.put(REMOTE_MSG_ID, remoteMsgId);
|
||||||
|
values.put(RELATIVE_FILE_PATH, relativeFilePath);
|
||||||
return values;
|
return values;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -205,6 +210,14 @@ public class Message extends AbstractEntity {
|
||||||
this.status = status;
|
this.status = status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setRelativeFilePath(String path) {
|
||||||
|
this.relativeFilePath = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRelativeFilePath() {
|
||||||
|
return this.relativeFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
public String getRemoteMsgId() {
|
public String getRemoteMsgId() {
|
||||||
return this.remoteMsgId;
|
return this.remoteMsgId;
|
||||||
}
|
}
|
||||||
|
@ -376,14 +389,14 @@ public class Message extends AbstractEntity {
|
||||||
}
|
}
|
||||||
String[] extensionParts = filename.split("\\.");
|
String[] extensionParts = filename.split("\\.");
|
||||||
if (extensionParts.length == 2
|
if (extensionParts.length == 2
|
||||||
&& Arrays.asList(Downloadable.VALID_EXTENSIONS).contains(
|
&& Arrays.asList(Downloadable.VALID_IMAGE_EXTENSIONS).contains(
|
||||||
extensionParts[extensionParts.length - 1])) {
|
extensionParts[extensionParts.length - 1])) {
|
||||||
return true;
|
return true;
|
||||||
} else if (extensionParts.length == 3
|
} else if (extensionParts.length == 3
|
||||||
&& Arrays
|
&& Arrays
|
||||||
.asList(Downloadable.VALID_CRYPTO_EXTENSIONS)
|
.asList(Downloadable.VALID_CRYPTO_EXTENSIONS)
|
||||||
.contains(extensionParts[extensionParts.length - 1])
|
.contains(extensionParts[extensionParts.length - 1])
|
||||||
&& Arrays.asList(Downloadable.VALID_EXTENSIONS).contains(
|
&& Arrays.asList(Downloadable.VALID_IMAGE_EXTENSIONS).contains(
|
||||||
extensionParts[extensionParts.length - 2])) {
|
extensionParts[extensionParts.length - 2])) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -37,6 +37,7 @@ public class HttpConnection implements Downloadable {
|
||||||
private DownloadableFile file;
|
private DownloadableFile file;
|
||||||
private int mStatus = Downloadable.STATUS_UNKNOWN;
|
private int mStatus = Downloadable.STATUS_UNKNOWN;
|
||||||
private boolean acceptedAutomatically = false;
|
private boolean acceptedAutomatically = false;
|
||||||
|
private int mProgress = 0;
|
||||||
|
|
||||||
public HttpConnection(HttpConnectionManager manager) {
|
public HttpConnection(HttpConnectionManager manager) {
|
||||||
this.mHttpConnectionManager = manager;
|
this.mHttpConnectionManager = manager;
|
||||||
|
@ -235,10 +236,14 @@ public class HttpConnection implements Downloadable {
|
||||||
if (os == null) {
|
if (os == null) {
|
||||||
throw new IOException();
|
throw new IOException();
|
||||||
}
|
}
|
||||||
|
long transmitted = 0;
|
||||||
|
long expected = file.getExpectedSize();
|
||||||
int count = -1;
|
int count = -1;
|
||||||
byte[] buffer = new byte[1024];
|
byte[] buffer = new byte[1024];
|
||||||
while ((count = is.read(buffer)) != -1) {
|
while ((count = is.read(buffer)) != -1) {
|
||||||
|
transmitted += count;
|
||||||
os.write(buffer, 0, count);
|
os.write(buffer, 0, count);
|
||||||
|
mProgress = (int) (expected * 100 / transmitted);
|
||||||
}
|
}
|
||||||
os.flush();
|
os.flush();
|
||||||
os.close();
|
os.close();
|
||||||
|
@ -272,4 +277,14 @@ public class HttpConnection implements Downloadable {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getProgress() {
|
||||||
|
return this.mProgress;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMimeType() {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -22,7 +22,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
||||||
private static DatabaseBackend instance = null;
|
private static DatabaseBackend instance = null;
|
||||||
|
|
||||||
private static final String DATABASE_NAME = "history";
|
private static final String DATABASE_NAME = "history";
|
||||||
private static final int DATABASE_VERSION = 9;
|
private static final int DATABASE_VERSION = 10;
|
||||||
|
|
||||||
private static String CREATE_CONTATCS_STATEMENT = "create table "
|
private static String CREATE_CONTATCS_STATEMENT = "create table "
|
||||||
+ Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, "
|
+ Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, "
|
||||||
|
@ -64,6 +64,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
||||||
+ " TEXT, " + Message.TRUE_COUNTERPART + " TEXT,"
|
+ " TEXT, " + Message.TRUE_COUNTERPART + " TEXT,"
|
||||||
+ Message.BODY + " TEXT, " + Message.ENCRYPTION + " NUMBER, "
|
+ Message.BODY + " TEXT, " + Message.ENCRYPTION + " NUMBER, "
|
||||||
+ Message.STATUS + " NUMBER," + Message.TYPE + " NUMBER, "
|
+ Message.STATUS + " NUMBER," + Message.TYPE + " NUMBER, "
|
||||||
|
+ Message.RELATIVE_FILE_PATH + " TEXT, "
|
||||||
+ Message.REMOTE_MSG_ID + " TEXT, FOREIGN KEY("
|
+ Message.REMOTE_MSG_ID + " TEXT, FOREIGN KEY("
|
||||||
+ Message.CONVERSATION + ") REFERENCES "
|
+ Message.CONVERSATION + ") REFERENCES "
|
||||||
+ Conversation.TABLENAME + "(" + Conversation.UUID
|
+ Conversation.TABLENAME + "(" + Conversation.UUID
|
||||||
|
@ -110,6 +111,10 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
||||||
db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN "
|
db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN "
|
||||||
+ Contact.LAST_PRESENCE + " TEXT");
|
+ Contact.LAST_PRESENCE + " TEXT");
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 10 && newVersion >= 10) {
|
||||||
|
db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "
|
||||||
|
+ Message.RELATIVE_FILE_PATH + " TEXT");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static synchronized DatabaseBackend getInstance(Context context) {
|
public static synchronized DatabaseBackend getInstance(Context context) {
|
||||||
|
|
|
@ -2,11 +2,13 @@ package eu.siacs.conversations.persistance;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
import java.net.URLConnection;
|
||||||
import java.security.DigestOutputStream;
|
import java.security.DigestOutputStream;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
@ -14,6 +16,7 @@ import java.text.SimpleDateFormat;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import android.content.ContentResolver;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.BitmapFactory;
|
import android.graphics.BitmapFactory;
|
||||||
|
@ -53,25 +56,34 @@ public class FileBackend {
|
||||||
}
|
}
|
||||||
|
|
||||||
public DownloadableFile getFile(Message message, boolean decrypted) {
|
public DownloadableFile getFile(Message message, boolean decrypted) {
|
||||||
StringBuilder filename = new StringBuilder();
|
String path = message.getRelativeFilePath();
|
||||||
filename.append(getConversationsDirectory());
|
if (path != null && !path.isEmpty()) {
|
||||||
filename.append(message.getUuid());
|
if (path.startsWith("/")) {
|
||||||
if ((decrypted) || (message.getEncryption() == Message.ENCRYPTION_NONE)) {
|
return new DownloadableFile(path);
|
||||||
filename.append(".webp");
|
} else {
|
||||||
|
return new DownloadableFile(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)+"/"+path);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (message.getEncryption() == Message.ENCRYPTION_OTR) {
|
StringBuilder filename = new StringBuilder();
|
||||||
|
filename.append(getConversationsDirectory());
|
||||||
|
filename.append(message.getUuid());
|
||||||
|
if ((decrypted) || (message.getEncryption() == Message.ENCRYPTION_NONE)) {
|
||||||
filename.append(".webp");
|
filename.append(".webp");
|
||||||
} else {
|
} else {
|
||||||
filename.append(".webp.pgp");
|
if (message.getEncryption() == Message.ENCRYPTION_OTR) {
|
||||||
|
filename.append(".webp");
|
||||||
|
} else {
|
||||||
|
filename.append(".webp.pgp");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return new DownloadableFile(filename.toString());
|
||||||
}
|
}
|
||||||
return new DownloadableFile(filename.toString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getConversationsDirectory() {
|
public static String getConversationsDirectory() {
|
||||||
return Environment.getExternalStoragePublicDirectory(
|
return Environment.getExternalStoragePublicDirectory(
|
||||||
Environment.DIRECTORY_PICTURES).getAbsolutePath()
|
Environment.DIRECTORY_PICTURES).getAbsolutePath()
|
||||||
+ "/Conversations/";
|
+ "/Conversations/";
|
||||||
}
|
}
|
||||||
|
|
||||||
public Bitmap resize(Bitmap originalBitmap, int size) {
|
public Bitmap resize(Bitmap originalBitmap, int size) {
|
||||||
|
@ -103,13 +115,34 @@ public class FileBackend {
|
||||||
return Bitmap.createBitmap(bitmap, 0, 0, w, h, mtx, true);
|
return Bitmap.createBitmap(bitmap, 0, 0, w, h, mtx, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getOriginalPath(Uri uri) {
|
||||||
|
String path = null;
|
||||||
|
if (uri.getScheme().equals("file")) {
|
||||||
|
path = uri.getPath();
|
||||||
|
} else {
|
||||||
|
String[] projection = {MediaStore.MediaColumns.DATA};
|
||||||
|
Cursor metaCursor = mXmppConnectionService.getContentResolver().query(uri,
|
||||||
|
projection, null, null, null);
|
||||||
|
if (metaCursor != null) {
|
||||||
|
try {
|
||||||
|
if (metaCursor.moveToFirst()) {
|
||||||
|
path = metaCursor.getString(0);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
metaCursor.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
public DownloadableFile copyImageToPrivateStorage(Message message, Uri image)
|
public DownloadableFile copyImageToPrivateStorage(Message message, Uri image)
|
||||||
throws ImageCopyException {
|
throws FileCopyException {
|
||||||
return this.copyImageToPrivateStorage(message, image, 0);
|
return this.copyImageToPrivateStorage(message, image, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private DownloadableFile copyImageToPrivateStorage(Message message,
|
private DownloadableFile copyImageToPrivateStorage(Message message,
|
||||||
Uri image, int sampleSize) throws ImageCopyException {
|
Uri image, int sampleSize) throws FileCopyException {
|
||||||
try {
|
try {
|
||||||
InputStream is = mXmppConnectionService.getContentResolver()
|
InputStream is = mXmppConnectionService.getContentResolver()
|
||||||
.openInputStream(image);
|
.openInputStream(image);
|
||||||
|
@ -125,7 +158,7 @@ public class FileBackend {
|
||||||
originalBitmap = BitmapFactory.decodeStream(is, null, options);
|
originalBitmap = BitmapFactory.decodeStream(is, null, options);
|
||||||
is.close();
|
is.close();
|
||||||
if (originalBitmap == null) {
|
if (originalBitmap == null) {
|
||||||
throw new ImageCopyException(R.string.error_not_an_image_file);
|
throw new FileCopyException(R.string.error_not_an_image_file);
|
||||||
}
|
}
|
||||||
Bitmap scalledBitmap = resize(originalBitmap, IMAGE_SIZE);
|
Bitmap scalledBitmap = resize(originalBitmap, IMAGE_SIZE);
|
||||||
originalBitmap = null;
|
originalBitmap = null;
|
||||||
|
@ -137,7 +170,7 @@ public class FileBackend {
|
||||||
boolean success = scalledBitmap.compress(
|
boolean success = scalledBitmap.compress(
|
||||||
Bitmap.CompressFormat.WEBP, 75, os);
|
Bitmap.CompressFormat.WEBP, 75, os);
|
||||||
if (!success) {
|
if (!success) {
|
||||||
throw new ImageCopyException(R.string.error_compressing_image);
|
throw new FileCopyException(R.string.error_compressing_image);
|
||||||
}
|
}
|
||||||
os.flush();
|
os.flush();
|
||||||
os.close();
|
os.close();
|
||||||
|
@ -147,18 +180,18 @@ public class FileBackend {
|
||||||
message.setBody(Long.toString(size) + ',' + width + ',' + height);
|
message.setBody(Long.toString(size) + ',' + width + ',' + height);
|
||||||
return file;
|
return file;
|
||||||
} catch (FileNotFoundException e) {
|
} catch (FileNotFoundException e) {
|
||||||
throw new ImageCopyException(R.string.error_file_not_found);
|
throw new FileCopyException(R.string.error_file_not_found);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new ImageCopyException(R.string.error_io_exception);
|
throw new FileCopyException(R.string.error_io_exception);
|
||||||
} catch (SecurityException e) {
|
} catch (SecurityException e) {
|
||||||
throw new ImageCopyException(
|
throw new FileCopyException(
|
||||||
R.string.error_security_exception_during_image_copy);
|
R.string.error_security_exception_during_image_copy);
|
||||||
} catch (OutOfMemoryError e) {
|
} catch (OutOfMemoryError e) {
|
||||||
++sampleSize;
|
++sampleSize;
|
||||||
if (sampleSize <= 3) {
|
if (sampleSize <= 3) {
|
||||||
return copyImageToPrivateStorage(message, image, sampleSize);
|
return copyImageToPrivateStorage(message, image, sampleSize);
|
||||||
} else {
|
} else {
|
||||||
throw new ImageCopyException(R.string.error_out_of_memory);
|
throw new FileCopyException(R.string.error_out_of_memory);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -400,11 +433,11 @@ public class FileBackend {
|
||||||
return Uri.parse("file://" + file.getAbsolutePath());
|
return Uri.parse("file://" + file.getAbsolutePath());
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ImageCopyException extends Exception {
|
public class FileCopyException extends Exception {
|
||||||
private static final long serialVersionUID = -1010013599132881427L;
|
private static final long serialVersionUID = -1010013599132881427L;
|
||||||
private int resId;
|
private int resId;
|
||||||
|
|
||||||
public ImageCopyException(int resId) {
|
public FileCopyException(int resId) {
|
||||||
this.resId = resId;
|
this.resId = resId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -56,6 +56,7 @@ import eu.siacs.conversations.entities.Bookmark;
|
||||||
import eu.siacs.conversations.entities.Contact;
|
import eu.siacs.conversations.entities.Contact;
|
||||||
import eu.siacs.conversations.entities.Conversation;
|
import eu.siacs.conversations.entities.Conversation;
|
||||||
import eu.siacs.conversations.entities.Downloadable;
|
import eu.siacs.conversations.entities.Downloadable;
|
||||||
|
import eu.siacs.conversations.entities.DownloadableFile;
|
||||||
import eu.siacs.conversations.entities.Message;
|
import eu.siacs.conversations.entities.Message;
|
||||||
import eu.siacs.conversations.entities.MucOptions;
|
import eu.siacs.conversations.entities.MucOptions;
|
||||||
import eu.siacs.conversations.entities.MucOptions.OnRenameListener;
|
import eu.siacs.conversations.entities.MucOptions.OnRenameListener;
|
||||||
|
@ -294,6 +295,27 @@ public class XmppConnectionService extends Service {
|
||||||
return this.mAvatarService;
|
return this.mAvatarService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Message attachFileToConversation(Conversation conversation, final Uri uri) {
|
||||||
|
String path = getFileBackend().getOriginalPath(uri);
|
||||||
|
if (path!=null) {
|
||||||
|
Log.d(Config.LOGTAG,"file path : "+path);
|
||||||
|
Message message;
|
||||||
|
if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) {
|
||||||
|
message = new Message(conversation, "",
|
||||||
|
Message.ENCRYPTION_DECRYPTED);
|
||||||
|
} else {
|
||||||
|
message = new Message(conversation, "",
|
||||||
|
conversation.getNextEncryption(forceEncryption()));
|
||||||
|
}
|
||||||
|
message.setCounterpart(conversation.getNextCounterpart());
|
||||||
|
message.setType(Message.TYPE_FILE);
|
||||||
|
message.setStatus(Message.STATUS_OFFERED);
|
||||||
|
message.setRelativeFilePath(path);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public Message attachImageToConversation(final Conversation conversation,
|
public Message attachImageToConversation(final Conversation conversation,
|
||||||
final Uri uri, final UiCallback<Message> callback) {
|
final Uri uri, final UiCallback<Message> callback) {
|
||||||
final Message message;
|
final Message message;
|
||||||
|
@ -312,13 +334,14 @@ public class XmppConnectionService extends Service {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
try {
|
try {
|
||||||
getFileBackend().copyImageToPrivateStorage(message, uri);
|
DownloadableFile file = getFileBackend().copyImageToPrivateStorage(message, uri);
|
||||||
|
message.setRelativeFilePath(file.getName());
|
||||||
if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) {
|
if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) {
|
||||||
getPgpEngine().encrypt(message, callback);
|
getPgpEngine().encrypt(message, callback);
|
||||||
} else {
|
} else {
|
||||||
callback.success(message);
|
callback.success(message);
|
||||||
}
|
}
|
||||||
} catch (FileBackend.ImageCopyException e) {
|
} catch (FileBackend.FileCopyException e) {
|
||||||
callback.error(e.getResId(), message);
|
callback.error(e.getResId(), message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -552,7 +575,7 @@ public class XmppConnectionService extends Service {
|
||||||
boolean send = false;
|
boolean send = false;
|
||||||
if (account.getStatus() == Account.STATUS_ONLINE
|
if (account.getStatus() == Account.STATUS_ONLINE
|
||||||
&& account.getXmppConnection() != null) {
|
&& account.getXmppConnection() != null) {
|
||||||
if (message.getType() == Message.TYPE_IMAGE) {
|
if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) {
|
||||||
if (message.getCounterpart() != null) {
|
if (message.getCounterpart() != null) {
|
||||||
if (message.getEncryption() == Message.ENCRYPTION_OTR) {
|
if (message.getEncryption() == Message.ENCRYPTION_OTR) {
|
||||||
if (!conv.hasValidOtrSession()) {
|
if (!conv.hasValidOtrSession()) {
|
||||||
|
@ -1988,5 +2011,15 @@ public class XmppConnectionService extends Service {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getProgress() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMimeType() {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ import android.os.SystemClock;
|
||||||
import android.provider.MediaStore;
|
import android.provider.MediaStore;
|
||||||
import android.support.v4.widget.SlidingPaneLayout;
|
import android.support.v4.widget.SlidingPaneLayout;
|
||||||
import android.support.v4.widget.SlidingPaneLayout.PanelSlideListener;
|
import android.support.v4.widget.SlidingPaneLayout.PanelSlideListener;
|
||||||
|
import android.util.Log;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
|
@ -31,6 +32,7 @@ import android.widget.Toast;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import eu.siacs.conversations.Config;
|
||||||
import eu.siacs.conversations.R;
|
import eu.siacs.conversations.R;
|
||||||
import eu.siacs.conversations.entities.Contact;
|
import eu.siacs.conversations.entities.Contact;
|
||||||
import eu.siacs.conversations.entities.Conversation;
|
import eu.siacs.conversations.entities.Conversation;
|
||||||
|
@ -52,13 +54,14 @@ public class ConversationActivity extends XmppActivity implements
|
||||||
public static final int REQUEST_SEND_MESSAGE = 0x0201;
|
public static final int REQUEST_SEND_MESSAGE = 0x0201;
|
||||||
public static final int REQUEST_DECRYPT_PGP = 0x0202;
|
public static final int REQUEST_DECRYPT_PGP = 0x0202;
|
||||||
public static final int REQUEST_ENCRYPT_MESSAGE = 0x0207;
|
public static final int REQUEST_ENCRYPT_MESSAGE = 0x0207;
|
||||||
private static final int REQUEST_ATTACH_FILE_DIALOG = 0x0203;
|
private static final int REQUEST_ATTACH_IMAGE_DIALOG = 0x0203;
|
||||||
private static final int REQUEST_IMAGE_CAPTURE = 0x0204;
|
private static final int REQUEST_IMAGE_CAPTURE = 0x0204;
|
||||||
private static final int REQUEST_RECORD_AUDIO = 0x0205;
|
private static final int REQUEST_RECORD_AUDIO = 0x0205;
|
||||||
private static final int REQUEST_SEND_PGP_IMAGE = 0x0206;
|
private static final int REQUEST_SEND_PGP_IMAGE = 0x0206;
|
||||||
|
private static final int REQUEST_ATTACH_FILE_DIALOG = 0x0208;
|
||||||
private static final int ATTACHMENT_CHOICE_CHOOSE_IMAGE = 0x0301;
|
private static final int ATTACHMENT_CHOICE_CHOOSE_IMAGE = 0x0301;
|
||||||
private static final int ATTACHMENT_CHOICE_TAKE_PHOTO = 0x0302;
|
private static final int ATTACHMENT_CHOICE_TAKE_PHOTO = 0x0302;
|
||||||
private static final int ATTACHMENT_CHOICE_RECORD_VOICE = 0x0303;
|
private static final int ATTACHMENT_CHOICE_CHOOSE_FILE = 0x0303;
|
||||||
private static final String STATE_OPEN_CONVERSATION = "state_open_conversation";
|
private static final String STATE_OPEN_CONVERSATION = "state_open_conversation";
|
||||||
private static final String STATE_PANEL_OPEN = "state_panel_open";
|
private static final String STATE_PANEL_OPEN = "state_panel_open";
|
||||||
private static final String STATE_PENDING_URI = "state_pending_uri";
|
private static final String STATE_PENDING_URI = "state_pending_uri";
|
||||||
|
@ -66,6 +69,7 @@ public class ConversationActivity extends XmppActivity implements
|
||||||
private String mOpenConverstaion = null;
|
private String mOpenConverstaion = null;
|
||||||
private boolean mPanelOpen = true;
|
private boolean mPanelOpen = true;
|
||||||
private Uri mPendingImageUri = null;
|
private Uri mPendingImageUri = null;
|
||||||
|
private Uri mPendingFileUri = null;
|
||||||
|
|
||||||
private View mContentView;
|
private View mContentView;
|
||||||
|
|
||||||
|
@ -306,13 +310,16 @@ public class ConversationActivity extends XmppActivity implements
|
||||||
Intent attachFileIntent = new Intent();
|
Intent attachFileIntent = new Intent();
|
||||||
attachFileIntent.setType("image/*");
|
attachFileIntent.setType("image/*");
|
||||||
attachFileIntent.setAction(Intent.ACTION_GET_CONTENT);
|
attachFileIntent.setAction(Intent.ACTION_GET_CONTENT);
|
||||||
|
Intent chooser = Intent.createChooser(attachFileIntent,
|
||||||
|
getString(R.string.attach_file));
|
||||||
|
startActivityForResult(chooser, REQUEST_ATTACH_IMAGE_DIALOG);
|
||||||
|
} else if (attachmentChoice == ATTACHMENT_CHOICE_CHOOSE_FILE) {
|
||||||
|
Intent attachFileIntent = new Intent();
|
||||||
|
attachFileIntent.setType("file/*");
|
||||||
|
attachFileIntent.setAction(Intent.ACTION_GET_CONTENT);
|
||||||
Intent chooser = Intent.createChooser(attachFileIntent,
|
Intent chooser = Intent.createChooser(attachFileIntent,
|
||||||
getString(R.string.attach_file));
|
getString(R.string.attach_file));
|
||||||
startActivityForResult(chooser, REQUEST_ATTACH_FILE_DIALOG);
|
startActivityForResult(chooser, REQUEST_ATTACH_FILE_DIALOG);
|
||||||
} else if (attachmentChoice == ATTACHMENT_CHOICE_RECORD_VOICE) {
|
|
||||||
Intent intent = new Intent(
|
|
||||||
MediaStore.Audio.Media.RECORD_SOUND_ACTION);
|
|
||||||
startActivityForResult(intent, REQUEST_RECORD_AUDIO);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -483,7 +490,7 @@ public class ConversationActivity extends XmppActivity implements
|
||||||
attachFile(ATTACHMENT_CHOICE_TAKE_PHOTO);
|
attachFile(ATTACHMENT_CHOICE_TAKE_PHOTO);
|
||||||
break;
|
break;
|
||||||
case R.id.attach_record_voice:
|
case R.id.attach_record_voice:
|
||||||
attachFile(ATTACHMENT_CHOICE_RECORD_VOICE);
|
attachFile(ATTACHMENT_CHOICE_CHOOSE_FILE);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
@ -675,14 +682,17 @@ public class ConversationActivity extends XmppActivity implements
|
||||||
} else {
|
} else {
|
||||||
showConversationsOverview();
|
showConversationsOverview();
|
||||||
mPendingImageUri = null;
|
mPendingImageUri = null;
|
||||||
|
mPendingFileUri = null;
|
||||||
setSelectedConversation(conversationList.get(0));
|
setSelectedConversation(conversationList.get(0));
|
||||||
this.mConversationFragment.reInit(getSelectedConversation());
|
this.mConversationFragment.reInit(getSelectedConversation());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mPendingImageUri != null) {
|
if (mPendingImageUri != null) {
|
||||||
attachImageToConversation(getSelectedConversation(),
|
attachImageToConversation(getSelectedConversation(),mPendingImageUri);
|
||||||
mPendingImageUri);
|
|
||||||
mPendingImageUri = null;
|
mPendingImageUri = null;
|
||||||
|
} else if (mPendingFileUri != null) {
|
||||||
|
attachFileToConversation(getSelectedConversation(),mPendingFileUri);
|
||||||
|
mPendingFileUri = null;
|
||||||
}
|
}
|
||||||
ExceptionHelper.checkForCrash(this, this.xmppConnectionService);
|
ExceptionHelper.checkForCrash(this, this.xmppConnectionService);
|
||||||
setIntent(new Intent());
|
setIntent(new Intent());
|
||||||
|
@ -726,13 +736,20 @@ public class ConversationActivity extends XmppActivity implements
|
||||||
selectedFragment.hideSnackbar();
|
selectedFragment.hideSnackbar();
|
||||||
selectedFragment.updateMessages();
|
selectedFragment.updateMessages();
|
||||||
}
|
}
|
||||||
} else if (requestCode == REQUEST_ATTACH_FILE_DIALOG) {
|
} else if (requestCode == REQUEST_ATTACH_IMAGE_DIALOG) {
|
||||||
mPendingImageUri = data.getData();
|
mPendingImageUri = data.getData();
|
||||||
if (xmppConnectionServiceBound) {
|
if (xmppConnectionServiceBound) {
|
||||||
attachImageToConversation(getSelectedConversation(),
|
attachImageToConversation(getSelectedConversation(),
|
||||||
mPendingImageUri);
|
mPendingImageUri);
|
||||||
mPendingImageUri = null;
|
mPendingImageUri = null;
|
||||||
}
|
}
|
||||||
|
} else if (requestCode == REQUEST_ATTACH_FILE_DIALOG) {
|
||||||
|
mPendingFileUri = data.getData();
|
||||||
|
if (xmppConnectionServiceBound) {
|
||||||
|
attachFileToConversation(getSelectedConversation(),
|
||||||
|
mPendingFileUri);
|
||||||
|
mPendingFileUri = null;
|
||||||
|
}
|
||||||
} else if (requestCode == REQUEST_SEND_PGP_IMAGE) {
|
} else if (requestCode == REQUEST_SEND_PGP_IMAGE) {
|
||||||
|
|
||||||
} else if (requestCode == ATTACHMENT_CHOICE_CHOOSE_IMAGE) {
|
} else if (requestCode == ATTACHMENT_CHOICE_CHOOSE_IMAGE) {
|
||||||
|
@ -754,9 +771,6 @@ public class ConversationActivity extends XmppActivity implements
|
||||||
Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
|
Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
|
||||||
intent.setData(mPendingImageUri);
|
intent.setData(mPendingImageUri);
|
||||||
sendBroadcast(intent);
|
sendBroadcast(intent);
|
||||||
} else if (requestCode == REQUEST_RECORD_AUDIO) {
|
|
||||||
attachAudioToConversation(getSelectedConversation(),
|
|
||||||
data.getData());
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (requestCode == REQUEST_IMAGE_CAPTURE) {
|
if (requestCode == REQUEST_IMAGE_CAPTURE) {
|
||||||
|
@ -765,8 +779,10 @@ public class ConversationActivity extends XmppActivity implements
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void attachAudioToConversation(Conversation conversation, Uri uri) {
|
private void attachFileToConversation(Conversation conversation, Uri uri) {
|
||||||
|
Log.d(Config.LOGTAG, "attachFileToConversation");
|
||||||
|
Message message = xmppConnectionService.attachFileToConversation(conversation,uri);
|
||||||
|
xmppConnectionService.sendMessage(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void attachImageToConversation(Conversation conversation, Uri uri) {
|
private void attachImageToConversation(Conversation conversation, Uri uri) {
|
||||||
|
|
|
@ -2,15 +2,18 @@ package eu.siacs.conversations.ui.adapter;
|
||||||
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.graphics.Typeface;
|
import android.graphics.Typeface;
|
||||||
|
import android.net.Uri;
|
||||||
import android.text.Spannable;
|
import android.text.Spannable;
|
||||||
import android.text.SpannableString;
|
import android.text.SpannableString;
|
||||||
import android.text.style.ForegroundColorSpan;
|
import android.text.style.ForegroundColorSpan;
|
||||||
import android.text.style.StyleSpan;
|
import android.text.style.StyleSpan;
|
||||||
import android.util.DisplayMetrics;
|
import android.util.DisplayMetrics;
|
||||||
|
import android.util.Log;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.View.OnClickListener;
|
import android.view.View.OnClickListener;
|
||||||
import android.view.View.OnLongClickListener;
|
import android.view.View.OnLongClickListener;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.webkit.MimeTypeMap;
|
||||||
import android.widget.ArrayAdapter;
|
import android.widget.ArrayAdapter;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
|
@ -18,6 +21,9 @@ import android.widget.LinearLayout;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import eu.siacs.conversations.Config;
|
import eu.siacs.conversations.Config;
|
||||||
|
@ -25,6 +31,7 @@ import eu.siacs.conversations.R;
|
||||||
import eu.siacs.conversations.entities.Contact;
|
import eu.siacs.conversations.entities.Contact;
|
||||||
import eu.siacs.conversations.entities.Conversation;
|
import eu.siacs.conversations.entities.Conversation;
|
||||||
import eu.siacs.conversations.entities.Downloadable;
|
import eu.siacs.conversations.entities.Downloadable;
|
||||||
|
import eu.siacs.conversations.entities.DownloadableFile;
|
||||||
import eu.siacs.conversations.entities.Message;
|
import eu.siacs.conversations.entities.Message;
|
||||||
import eu.siacs.conversations.entities.Message.ImageParams;
|
import eu.siacs.conversations.entities.Message.ImageParams;
|
||||||
import eu.siacs.conversations.ui.ConversationActivity;
|
import eu.siacs.conversations.ui.ConversationActivity;
|
||||||
|
@ -181,13 +188,13 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void displayInfoMessage(ViewHolder viewHolder, int r) {
|
private void displayInfoMessage(ViewHolder viewHolder, String text) {
|
||||||
if (viewHolder.download_button != null) {
|
if (viewHolder.download_button != null) {
|
||||||
viewHolder.download_button.setVisibility(View.GONE);
|
viewHolder.download_button.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
viewHolder.image.setVisibility(View.GONE);
|
viewHolder.image.setVisibility(View.GONE);
|
||||||
viewHolder.messageBody.setVisibility(View.VISIBLE);
|
viewHolder.messageBody.setVisibility(View.VISIBLE);
|
||||||
viewHolder.messageBody.setText(getContext().getString(r));
|
viewHolder.messageBody.setText(text);
|
||||||
viewHolder.messageBody.setTextColor(activity.getSecondaryTextColor());
|
viewHolder.messageBody.setTextColor(activity.getSecondaryTextColor());
|
||||||
viewHolder.messageBody.setTypeface(null, Typeface.ITALIC);
|
viewHolder.messageBody.setTypeface(null, Typeface.ITALIC);
|
||||||
viewHolder.messageBody.setTextIsSelectable(false);
|
viewHolder.messageBody.setTextIsSelectable(false);
|
||||||
|
@ -252,11 +259,11 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void displayDownloadableMessage(ViewHolder viewHolder,
|
private void displayDownloadableMessage(ViewHolder viewHolder,
|
||||||
final Message message, int resid) {
|
final Message message, String text) {
|
||||||
viewHolder.image.setVisibility(View.GONE);
|
viewHolder.image.setVisibility(View.GONE);
|
||||||
viewHolder.messageBody.setVisibility(View.GONE);
|
viewHolder.messageBody.setVisibility(View.GONE);
|
||||||
viewHolder.download_button.setVisibility(View.VISIBLE);
|
viewHolder.download_button.setVisibility(View.VISIBLE);
|
||||||
viewHolder.download_button.setText(resid);
|
viewHolder.download_button.setText(text);
|
||||||
viewHolder.download_button.setOnClickListener(new OnClickListener() {
|
viewHolder.download_button.setOnClickListener(new OnClickListener() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -267,6 +274,21 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
||||||
viewHolder.download_button.setOnLongClickListener(openContextMenu);
|
viewHolder.download_button.setOnLongClickListener(openContextMenu);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void displayOpenableMessage(ViewHolder viewHolder,final Message message) {
|
||||||
|
viewHolder.image.setVisibility(View.GONE);
|
||||||
|
viewHolder.messageBody.setVisibility(View.GONE);
|
||||||
|
viewHolder.download_button.setVisibility(View.VISIBLE);
|
||||||
|
viewHolder.download_button.setText(activity.getString(R.string.open_file,activity.xmppConnectionService.getFileBackend().getFile(message).getMimeType()));
|
||||||
|
viewHolder.download_button.setOnClickListener(new OnClickListener() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
openDonwloadable(message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
viewHolder.download_button.setOnLongClickListener(openContextMenu);
|
||||||
|
}
|
||||||
|
|
||||||
private void displayImageMessage(ViewHolder viewHolder,
|
private void displayImageMessage(ViewHolder viewHolder,
|
||||||
final Message message) {
|
final Message message) {
|
||||||
if (viewHolder.download_button != null) {
|
if (viewHolder.download_button != null) {
|
||||||
|
@ -455,42 +477,46 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.getType() == Message.TYPE_IMAGE
|
if (item.getDownloadable() != null) {
|
||||||
|| item.getDownloadable() != null) {
|
|
||||||
Downloadable d = item.getDownloadable();
|
Downloadable d = item.getDownloadable();
|
||||||
if (d != null && d.getStatus() == Downloadable.STATUS_DOWNLOADING) {
|
if (d.getStatus() == Downloadable.STATUS_DOWNLOADING) {
|
||||||
displayInfoMessage(viewHolder, R.string.receiving_image);
|
if (item.getType() == Message.TYPE_FILE) {
|
||||||
} else if (d != null
|
displayInfoMessage(viewHolder,activity.getString(R.string.receiving_file,d.getMimeType(),d.getProgress()));
|
||||||
&& d.getStatus() == Downloadable.STATUS_CHECKING) {
|
} else {
|
||||||
displayInfoMessage(viewHolder, R.string.checking_image);
|
displayInfoMessage(viewHolder,activity.getString(R.string.receiving_image,d.getProgress()));
|
||||||
} else if (d != null
|
}
|
||||||
&& d.getStatus() == Downloadable.STATUS_DELETED) {
|
} else if (d.getStatus() == Downloadable.STATUS_CHECKING) {
|
||||||
displayInfoMessage(viewHolder, R.string.image_file_deleted);
|
displayInfoMessage(viewHolder,activity.getString(R.string.checking_image));
|
||||||
} else if (d != null && d.getStatus() == Downloadable.STATUS_OFFER) {
|
} else if (d.getStatus() == Downloadable.STATUS_DELETED) {
|
||||||
displayDownloadableMessage(viewHolder, item,
|
displayInfoMessage(viewHolder,activity.getString(R.string.image_file_deleted));
|
||||||
R.string.download_image);
|
} else if (d.getStatus() == Downloadable.STATUS_OFFER) {
|
||||||
} else if (d != null
|
if (item.getType() == Message.TYPE_FILE) {
|
||||||
&& d.getStatus() == Downloadable.STATUS_OFFER_CHECK_FILESIZE) {
|
displayDownloadableMessage(viewHolder,item,activity.getString(R.string.download_file,d.getMimeType()));
|
||||||
displayDownloadableMessage(viewHolder, item,
|
} else {
|
||||||
R.string.check_image_filesize);
|
displayDownloadableMessage(viewHolder, item,activity.getString(R.string.download_image));
|
||||||
} else if (d != null && d.getStatus() == Downloadable.STATUS_FAILED) {
|
}
|
||||||
displayInfoMessage(viewHolder, R.string.image_transmission_failed);
|
} else if (d.getStatus() == Downloadable.STATUS_OFFER_CHECK_FILESIZE) {
|
||||||
} else if ((item.getEncryption() == Message.ENCRYPTION_DECRYPTED)
|
displayDownloadableMessage(viewHolder, item,activity.getString(R.string.check_image_filesize));
|
||||||
|| (item.getEncryption() == Message.ENCRYPTION_NONE)
|
} else if (d.getStatus() == Downloadable.STATUS_FAILED) {
|
||||||
|| (item.getEncryption() == Message.ENCRYPTION_OTR)) {
|
displayInfoMessage(viewHolder, activity.getString(R.string.image_transmission_failed));
|
||||||
displayImageMessage(viewHolder, item);
|
|
||||||
} else if (item.getEncryption() == Message.ENCRYPTION_PGP) {
|
|
||||||
displayInfoMessage(viewHolder, R.string.encrypted_message);
|
|
||||||
} else {
|
|
||||||
displayDecryptionFailed(viewHolder);
|
|
||||||
}
|
}
|
||||||
|
} else if (item.getType() == Message.TYPE_IMAGE) {
|
||||||
|
if (item.getEncryption() == Message.ENCRYPTION_PGP) {
|
||||||
|
displayInfoMessage(viewHolder,activity.getString(R.string.encrypted_message));
|
||||||
|
} else if (item.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) {
|
||||||
|
displayDecryptionFailed(viewHolder);
|
||||||
|
} else {
|
||||||
|
displayImageMessage(viewHolder, item);
|
||||||
|
}
|
||||||
|
} else if (item.getType() == Message.TYPE_FILE) {
|
||||||
|
displayOpenableMessage(viewHolder,item);
|
||||||
} else {
|
} else {
|
||||||
if (item.getEncryption() == Message.ENCRYPTION_PGP) {
|
if (item.getEncryption() == Message.ENCRYPTION_PGP) {
|
||||||
if (activity.hasPgp()) {
|
if (activity.hasPgp()) {
|
||||||
displayInfoMessage(viewHolder, R.string.encrypted_message);
|
displayInfoMessage(viewHolder,activity.getString(R.string.encrypted_message));
|
||||||
} else {
|
} else {
|
||||||
displayInfoMessage(viewHolder,
|
displayInfoMessage(viewHolder,
|
||||||
R.string.install_openkeychain);
|
activity.getString(R.string.install_openkeychain));
|
||||||
if (viewHolder != null) {
|
if (viewHolder != null) {
|
||||||
viewHolder.message_box
|
viewHolder.message_box
|
||||||
.setOnClickListener(new OnClickListener() {
|
.setOnClickListener(new OnClickListener() {
|
||||||
|
@ -524,6 +550,13 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void openDonwloadable(Message message) {
|
||||||
|
DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message);
|
||||||
|
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||||
|
intent.setDataAndType(Uri.fromFile(file), file.getMimeType());
|
||||||
|
getContext().startActivity(intent);
|
||||||
|
}
|
||||||
|
|
||||||
public interface OnContactPictureClicked {
|
public interface OnContactPictureClicked {
|
||||||
public void onContactPictureClicked(Message message);
|
public void onContactPictureClicked(Message message);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.graphics.BitmapFactory;
|
import android.graphics.BitmapFactory;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.os.SystemClock;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import eu.siacs.conversations.Config;
|
import eu.siacs.conversations.Config;
|
||||||
import eu.siacs.conversations.entities.Account;
|
import eu.siacs.conversations.entities.Account;
|
||||||
|
@ -29,9 +30,6 @@ import eu.siacs.conversations.xmpp.stanzas.IqPacket;
|
||||||
|
|
||||||
public class JingleConnection implements Downloadable {
|
public class JingleConnection implements Downloadable {
|
||||||
|
|
||||||
private final String[] extensions = { "webp", "jpeg", "jpg", "png" };
|
|
||||||
private final String[] cryptoExtensions = { "pgp", "gpg", "otr" };
|
|
||||||
|
|
||||||
private JingleConnectionManager mJingleConnectionManager;
|
private JingleConnectionManager mJingleConnectionManager;
|
||||||
private XmppConnectionService mXmppConnectionService;
|
private XmppConnectionService mXmppConnectionService;
|
||||||
|
|
||||||
|
@ -62,6 +60,9 @@ public class JingleConnection implements Downloadable {
|
||||||
private String contentName;
|
private String contentName;
|
||||||
private String contentCreator;
|
private String contentCreator;
|
||||||
|
|
||||||
|
private int mProgress = 0;
|
||||||
|
private long mLastGuiRefresh = 0;
|
||||||
|
|
||||||
private boolean receivedCandidate = false;
|
private boolean receivedCandidate = false;
|
||||||
private boolean sentCandidate = false;
|
private boolean sentCandidate = false;
|
||||||
|
|
||||||
|
@ -258,7 +259,6 @@ public class JingleConnection implements Downloadable {
|
||||||
packet.getFrom().toBareJid(), false);
|
packet.getFrom().toBareJid(), false);
|
||||||
this.message = new Message(conversation, "", Message.ENCRYPTION_NONE);
|
this.message = new Message(conversation, "", Message.ENCRYPTION_NONE);
|
||||||
this.message.setStatus(Message.STATUS_RECEIVED);
|
this.message.setStatus(Message.STATUS_RECEIVED);
|
||||||
this.message.setType(Message.TYPE_IMAGE);
|
|
||||||
this.mStatus = Downloadable.STATUS_OFFER;
|
this.mStatus = Downloadable.STATUS_OFFER;
|
||||||
this.message.setDownloadable(this);
|
this.message.setDownloadable(this);
|
||||||
final Jid from = packet.getFrom();
|
final Jid from = packet.getFrom();
|
||||||
|
@ -278,68 +278,71 @@ public class JingleConnection implements Downloadable {
|
||||||
Element fileSize = fileOffer.findChild("size");
|
Element fileSize = fileOffer.findChild("size");
|
||||||
Element fileNameElement = fileOffer.findChild("name");
|
Element fileNameElement = fileOffer.findChild("name");
|
||||||
if (fileNameElement != null) {
|
if (fileNameElement != null) {
|
||||||
boolean supportedFile = false;
|
|
||||||
String[] filename = fileNameElement.getContent()
|
String[] filename = fileNameElement.getContent()
|
||||||
.toLowerCase(Locale.US).split("\\.");
|
.toLowerCase(Locale.US).split("\\.");
|
||||||
if (Arrays.asList(this.extensions).contains(
|
if (Arrays.asList(VALID_IMAGE_EXTENSIONS).contains(
|
||||||
filename[filename.length - 1])) {
|
filename[filename.length - 1])) {
|
||||||
supportedFile = true;
|
message.setType(Message.TYPE_IMAGE);
|
||||||
} else if (Arrays.asList(this.cryptoExtensions).contains(
|
} else if (Arrays.asList(VALID_CRYPTO_EXTENSIONS).contains(
|
||||||
filename[filename.length - 1])) {
|
filename[filename.length - 1])) {
|
||||||
if (filename.length == 3) {
|
if (filename.length == 3) {
|
||||||
if (Arrays.asList(this.extensions).contains(
|
if (Arrays.asList(VALID_IMAGE_EXTENSIONS).contains(
|
||||||
filename[filename.length - 2])) {
|
filename[filename.length - 2])) {
|
||||||
supportedFile = true;
|
message.setType(Message.TYPE_IMAGE);
|
||||||
if (filename[filename.length - 1].equals("otr")) {
|
|
||||||
Log.d(Config.LOGTAG, "receiving otr file");
|
|
||||||
this.message
|
|
||||||
.setEncryption(Message.ENCRYPTION_OTR);
|
|
||||||
} else {
|
|
||||||
this.message
|
|
||||||
.setEncryption(Message.ENCRYPTION_PGP);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (supportedFile) {
|
|
||||||
long size = Long.parseLong(fileSize.getContent());
|
|
||||||
message.setBody(Long.toString(size));
|
|
||||||
conversation.add(message);
|
|
||||||
mXmppConnectionService.updateConversationUi();
|
|
||||||
if (size <= this.mJingleConnectionManager
|
|
||||||
.getAutoAcceptFileSize()) {
|
|
||||||
Log.d(Config.LOGTAG, "auto accepting file from "
|
|
||||||
+ packet.getFrom());
|
|
||||||
this.acceptedAutomatically = true;
|
|
||||||
this.sendAccept();
|
|
||||||
} else {
|
|
||||||
message.markUnread();
|
|
||||||
Log.d(Config.LOGTAG,
|
|
||||||
"not auto accepting new file offer with size: "
|
|
||||||
+ size
|
|
||||||
+ " allowed size:"
|
|
||||||
+ this.mJingleConnectionManager
|
|
||||||
.getAutoAcceptFileSize());
|
|
||||||
this.mXmppConnectionService.getNotificationService()
|
|
||||||
.push(message);
|
|
||||||
}
|
|
||||||
this.file = this.mXmppConnectionService.getFileBackend()
|
|
||||||
.getFile(message, false);
|
|
||||||
if (message.getEncryption() == Message.ENCRYPTION_OTR) {
|
|
||||||
byte[] key = conversation.getSymmetricKey();
|
|
||||||
if (key == null) {
|
|
||||||
this.sendCancel();
|
|
||||||
this.cancel();
|
|
||||||
return;
|
|
||||||
} else {
|
} else {
|
||||||
this.file.setKey(key);
|
message.setType(Message.TYPE_FILE);
|
||||||
|
}
|
||||||
|
if (filename[filename.length - 1].equals("otr")) {
|
||||||
|
message.setEncryption(Message.ENCRYPTION_OTR);
|
||||||
|
} else {
|
||||||
|
message.setEncryption(Message.ENCRYPTION_PGP);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.file.setExpectedSize(size);
|
|
||||||
} else {
|
} else {
|
||||||
this.sendCancel();
|
message.setType(Message.TYPE_FILE);
|
||||||
this.cancel();
|
|
||||||
}
|
}
|
||||||
|
if (message.getType() == Message.TYPE_FILE) {
|
||||||
|
String suffix = "";
|
||||||
|
if (!fileNameElement.getContent().isEmpty()) {
|
||||||
|
String parts[] = fileNameElement.getContent().split("/");
|
||||||
|
suffix = parts[parts.length - 1];
|
||||||
|
}
|
||||||
|
message.setRelativeFilePath(message.getUuid()+"_"+suffix);
|
||||||
|
}
|
||||||
|
long size = Long.parseLong(fileSize.getContent());
|
||||||
|
message.setBody(Long.toString(size));
|
||||||
|
conversation.add(message);
|
||||||
|
mXmppConnectionService.updateConversationUi();
|
||||||
|
if (size <= this.mJingleConnectionManager
|
||||||
|
.getAutoAcceptFileSize()) {
|
||||||
|
Log.d(Config.LOGTAG, "auto accepting file from "
|
||||||
|
+ packet.getFrom());
|
||||||
|
this.acceptedAutomatically = true;
|
||||||
|
this.sendAccept();
|
||||||
|
} else {
|
||||||
|
message.markUnread();
|
||||||
|
Log.d(Config.LOGTAG,
|
||||||
|
"not auto accepting new file offer with size: "
|
||||||
|
+ size
|
||||||
|
+ " allowed size:"
|
||||||
|
+ this.mJingleConnectionManager
|
||||||
|
.getAutoAcceptFileSize());
|
||||||
|
this.mXmppConnectionService.getNotificationService()
|
||||||
|
.push(message);
|
||||||
|
}
|
||||||
|
this.file = this.mXmppConnectionService.getFileBackend()
|
||||||
|
.getFile(message, false);
|
||||||
|
if (message.getEncryption() == Message.ENCRYPTION_OTR) {
|
||||||
|
byte[] key = conversation.getSymmetricKey();
|
||||||
|
if (key == null) {
|
||||||
|
this.sendCancel();
|
||||||
|
this.cancel();
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
this.file.setKey(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.file.setExpectedSize(size);
|
||||||
} else {
|
} else {
|
||||||
this.sendCancel();
|
this.sendCancel();
|
||||||
this.cancel();
|
this.cancel();
|
||||||
|
@ -354,7 +357,7 @@ public class JingleConnection implements Downloadable {
|
||||||
this.mXmppConnectionService.markMessage(this.message, Message.STATUS_OFFERED);
|
this.mXmppConnectionService.markMessage(this.message, Message.STATUS_OFFERED);
|
||||||
JinglePacket packet = this.bootstrapPacket("session-initiate");
|
JinglePacket packet = this.bootstrapPacket("session-initiate");
|
||||||
Content content = new Content(this.contentCreator, this.contentName);
|
Content content = new Content(this.contentCreator, this.contentName);
|
||||||
if (message.getType() == Message.TYPE_IMAGE) {
|
if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) {
|
||||||
content.setTransportId(this.transportId);
|
content.setTransportId(this.transportId);
|
||||||
this.file = this.mXmppConnectionService.getFileBackend().getFile(
|
this.file = this.mXmppConnectionService.getFileBackend().getFile(
|
||||||
message, false);
|
message, false);
|
||||||
|
@ -856,6 +859,14 @@ public class JingleConnection implements Downloadable {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void updateProgress(int i) {
|
||||||
|
this.mProgress = i;
|
||||||
|
if (SystemClock.elapsedRealtime() - this.mLastGuiRefresh > 1000) {
|
||||||
|
this.mLastGuiRefresh = SystemClock.elapsedRealtime();
|
||||||
|
mXmppConnectionService.updateConversationUi();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface OnProxyActivated {
|
interface OnProxyActivated {
|
||||||
public void success();
|
public void success();
|
||||||
|
|
||||||
|
@ -900,4 +911,14 @@ public class JingleConnection implements Downloadable {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getProgress() {
|
||||||
|
return this.mProgress;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMimeType() {
|
||||||
|
return this.file.getMimeType();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ import eu.siacs.conversations.utils.CryptoHelper;
|
||||||
|
|
||||||
public class JingleSocks5Transport extends JingleTransport {
|
public class JingleSocks5Transport extends JingleTransport {
|
||||||
private JingleCandidate candidate;
|
private JingleCandidate candidate;
|
||||||
|
private JingleConnection connection;
|
||||||
private String destination;
|
private String destination;
|
||||||
private OutputStream outputStream;
|
private OutputStream outputStream;
|
||||||
private InputStream inputStream;
|
private InputStream inputStream;
|
||||||
|
@ -25,6 +26,7 @@ public class JingleSocks5Transport extends JingleTransport {
|
||||||
public JingleSocks5Transport(JingleConnection jingleConnection,
|
public JingleSocks5Transport(JingleConnection jingleConnection,
|
||||||
JingleCandidate candidate) {
|
JingleCandidate candidate) {
|
||||||
this.candidate = candidate;
|
this.candidate = candidate;
|
||||||
|
this.connection = jingleConnection;
|
||||||
try {
|
try {
|
||||||
MessageDigest mDigest = MessageDigest.getInstance("SHA-1");
|
MessageDigest mDigest = MessageDigest.getInstance("SHA-1");
|
||||||
StringBuilder destBuilder = new StringBuilder();
|
StringBuilder destBuilder = new StringBuilder();
|
||||||
|
@ -102,11 +104,15 @@ public class JingleSocks5Transport extends JingleTransport {
|
||||||
callback.onFileTransferAborted();
|
callback.onFileTransferAborted();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
long size = file.getSize();
|
||||||
|
double transmitted = 0;
|
||||||
int count;
|
int count;
|
||||||
byte[] buffer = new byte[8192];
|
byte[] buffer = new byte[8192];
|
||||||
while ((count = fileInputStream.read(buffer)) > 0) {
|
while ((count = fileInputStream.read(buffer)) > 0) {
|
||||||
outputStream.write(buffer, 0, count);
|
outputStream.write(buffer, 0, count);
|
||||||
digest.update(buffer, 0, count);
|
digest.update(buffer, 0, count);
|
||||||
|
transmitted += count;
|
||||||
|
connection.updateProgress((int) (((transmitted) / size) * 100));
|
||||||
}
|
}
|
||||||
outputStream.flush();
|
outputStream.flush();
|
||||||
file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest()));
|
file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest()));
|
||||||
|
@ -151,6 +157,7 @@ public class JingleSocks5Transport extends JingleTransport {
|
||||||
callback.onFileTransferAborted();
|
callback.onFileTransferAborted();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
double size = file.getExpectedSize();
|
||||||
long remainingSize = file.getExpectedSize();
|
long remainingSize = file.getExpectedSize();
|
||||||
byte[] buffer = new byte[8192];
|
byte[] buffer = new byte[8192];
|
||||||
int count = buffer.length;
|
int count = buffer.length;
|
||||||
|
@ -164,6 +171,7 @@ public class JingleSocks5Transport extends JingleTransport {
|
||||||
digest.update(buffer, 0, count);
|
digest.update(buffer, 0, count);
|
||||||
remainingSize -= count;
|
remainingSize -= count;
|
||||||
}
|
}
|
||||||
|
connection.updateProgress((int) (((size - remainingSize) / size) * 100));
|
||||||
}
|
}
|
||||||
fileOutputStream.flush();
|
fileOutputStream.flush();
|
||||||
fileOutputStream.close();
|
fileOutputStream.close();
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
android:title="@string/attach_take_picture"/>
|
android:title="@string/attach_take_picture"/>
|
||||||
<item
|
<item
|
||||||
android:id="@+id/attach_record_voice"
|
android:id="@+id/attach_record_voice"
|
||||||
android:title="@string/attach_record_voice"
|
android:title="@string/choose_file"/>
|
||||||
android:visible="false"/>
|
|
||||||
|
|
||||||
</menu>
|
</menu>
|
|
@ -58,7 +58,7 @@
|
||||||
<string name="add_contact">Add contact</string>
|
<string name="add_contact">Add contact</string>
|
||||||
<string name="send_failed">delivery failed</string>
|
<string name="send_failed">delivery failed</string>
|
||||||
<string name="send_rejected">rejected</string>
|
<string name="send_rejected">rejected</string>
|
||||||
<string name="receiving_image">Receiving image file. Please wait…</string>
|
<string name="receiving_image">Receiving image file (%1$d%%)</string>
|
||||||
<string name="preparing_image">Preparing image for transmission</string>
|
<string name="preparing_image">Preparing image for transmission</string>
|
||||||
<string name="action_clear_history">Clear history</string>
|
<string name="action_clear_history">Clear history</string>
|
||||||
<string name="clear_conversation_history">Clear Conversation History</string>
|
<string name="clear_conversation_history">Clear Conversation History</string>
|
||||||
|
@ -311,4 +311,8 @@
|
||||||
<string name="scan_qr_code">Scan QR code</string>
|
<string name="scan_qr_code">Scan QR code</string>
|
||||||
<string name="show_qr_code">Show QR code</string>
|
<string name="show_qr_code">Show QR code</string>
|
||||||
<string name="account_details">Account details</string>
|
<string name="account_details">Account details</string>
|
||||||
|
<string name="choose_file">Choose file</string>
|
||||||
|
<string name="receiving_file">Receiving %1$s file (%2$d%%)</string>
|
||||||
|
<string name="download_file">Download %s file</string>
|
||||||
|
<string name="open_file">Open %s file</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Loading…
Reference in a new issue