Attachment download
1
FAQ.md
|
@ -21,6 +21,7 @@ The low priority status bar notification shows the number of pending operations,
|
||||||
* Move message to another remote folder
|
* Move message to another remote folder
|
||||||
* Delete message from remote folder
|
* Delete message from remote folder
|
||||||
* Send message
|
* Send message
|
||||||
|
* Download attachment
|
||||||
|
|
||||||
<a name="FAQ3"></a>
|
<a name="FAQ3"></a>
|
||||||
**(3) What is a valid security certificate?**
|
**(3) What is a valid security certificate?**
|
||||||
|
|
|
@ -28,15 +28,19 @@ import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
public class AdapterAttachment extends RecyclerView.Adapter<AdapterAttachment.ViewHolder> {
|
public class AdapterAttachment extends RecyclerView.Adapter<AdapterAttachment.ViewHolder> {
|
||||||
private Context context;
|
private Context context;
|
||||||
|
private ExecutorService executor = Executors.newCachedThreadPool();
|
||||||
|
|
||||||
private List<EntityAttachment> all = new ArrayList<>();
|
private List<EntityAttachment> all = new ArrayList<>();
|
||||||
private List<EntityAttachment> filtered = new ArrayList<>();
|
private List<EntityAttachment> filtered = new ArrayList<>();
|
||||||
|
@ -45,30 +49,39 @@ public class AdapterAttachment extends RecyclerView.Adapter<AdapterAttachment.Vi
|
||||||
implements View.OnClickListener {
|
implements View.OnClickListener {
|
||||||
View itemView;
|
View itemView;
|
||||||
TextView tvName;
|
TextView tvName;
|
||||||
TextView tvType;
|
TextView tvSize;
|
||||||
|
ImageView ivDownload;
|
||||||
|
|
||||||
ViewHolder(View itemView) {
|
ViewHolder(View itemView) {
|
||||||
super(itemView);
|
super(itemView);
|
||||||
|
|
||||||
this.itemView = itemView;
|
this.itemView = itemView;
|
||||||
tvName = itemView.findViewById(R.id.tvName);
|
tvName = itemView.findViewById(R.id.tvName);
|
||||||
tvType = itemView.findViewById(R.id.tvType);
|
tvSize = itemView.findViewById(R.id.tvSize);
|
||||||
|
ivDownload = itemView.findViewById(R.id.ivDownload);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void wire() {
|
private void wire() {
|
||||||
itemView.setOnClickListener(this);
|
itemView.setOnClickListener(this);
|
||||||
|
ivDownload.setOnClickListener(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void unwire() {
|
private void unwire() {
|
||||||
itemView.setOnClickListener(null);
|
itemView.setOnClickListener(null);
|
||||||
|
ivDownload.setOnClickListener(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View view) {
|
public void onClick(View view) {
|
||||||
EntityAttachment attachment = filtered.get(getLayoutPosition());
|
final EntityAttachment attachment = filtered.get(getLayoutPosition());
|
||||||
if (attachment.content == null) {
|
if (attachment != null && attachment.content == null)
|
||||||
|
executor.submit(new Runnable() {
|
||||||
}
|
@Override
|
||||||
|
public void run() {
|
||||||
|
EntityMessage message = DB.getInstance(context).message().getMessage(attachment.message);
|
||||||
|
EntityOperation.queue(context, message, EntityOperation.ATTACHMENT, attachment.sequence);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -175,7 +188,11 @@ public class AdapterAttachment extends RecyclerView.Adapter<AdapterAttachment.Vi
|
||||||
|
|
||||||
EntityAttachment attachment = filtered.get(position);
|
EntityAttachment attachment = filtered.get(position);
|
||||||
holder.tvName.setText(attachment.name);
|
holder.tvName.setText(attachment.name);
|
||||||
holder.tvType.setText(attachment.type);
|
holder.tvSize.setVisibility((attachment.content == null ? View.GONE : View.VISIBLE));
|
||||||
|
holder.ivDownload.setVisibility((attachment.content == null ? View.VISIBLE : View.GONE));
|
||||||
|
|
||||||
|
if (attachment.content != null)
|
||||||
|
holder.tvSize.setText(Helper.humanReadableByteCount(attachment.content.length, false));
|
||||||
|
|
||||||
holder.wire();
|
holder.wire();
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ import android.arch.persistence.room.Dao;
|
||||||
import android.arch.persistence.room.Insert;
|
import android.arch.persistence.room.Insert;
|
||||||
import android.arch.persistence.room.OnConflictStrategy;
|
import android.arch.persistence.room.OnConflictStrategy;
|
||||||
import android.arch.persistence.room.Query;
|
import android.arch.persistence.room.Query;
|
||||||
|
import android.arch.persistence.room.Update;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@ -32,6 +33,12 @@ public interface DaoAttachment {
|
||||||
@Query("SELECT * FROM attachment WHERE message = :message")
|
@Query("SELECT * FROM attachment WHERE message = :message")
|
||||||
LiveData<List<EntityAttachment>> liveAttachments(long message);
|
LiveData<List<EntityAttachment>> liveAttachments(long message);
|
||||||
|
|
||||||
|
@Query("SELECT * FROM attachment WHERE message = :message AND sequence = :sequence")
|
||||||
|
EntityAttachment getAttachment(long message, int sequence);
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
long insertAttachment(EntityAttachment attachment);
|
long insertAttachment(EntityAttachment attachment);
|
||||||
|
|
||||||
|
@Update
|
||||||
|
void updateAttachment(EntityAttachment attachment);
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,10 +21,13 @@ package eu.faircode.email;
|
||||||
|
|
||||||
import android.arch.persistence.room.Entity;
|
import android.arch.persistence.room.Entity;
|
||||||
import android.arch.persistence.room.ForeignKey;
|
import android.arch.persistence.room.ForeignKey;
|
||||||
|
import android.arch.persistence.room.Ignore;
|
||||||
import android.arch.persistence.room.Index;
|
import android.arch.persistence.room.Index;
|
||||||
import android.arch.persistence.room.PrimaryKey;
|
import android.arch.persistence.room.PrimaryKey;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
import javax.mail.BodyPart;
|
||||||
|
|
||||||
import static android.arch.persistence.room.ForeignKey.CASCADE;
|
import static android.arch.persistence.room.ForeignKey.CASCADE;
|
||||||
|
|
||||||
@Entity(
|
@Entity(
|
||||||
|
@ -50,4 +53,7 @@ public class EntityAttachment {
|
||||||
@NonNull
|
@NonNull
|
||||||
public String type;
|
public String type;
|
||||||
public byte[] content;
|
public byte[] content;
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
BodyPart part;
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,6 +58,7 @@ public class EntityOperation {
|
||||||
public static final String MOVE = "move";
|
public static final String MOVE = "move";
|
||||||
public static final String DELETE = "delete";
|
public static final String DELETE = "delete";
|
||||||
public static final String SEND = "send";
|
public static final String SEND = "send";
|
||||||
|
public static final String ATTACHMENT = "attachment";
|
||||||
|
|
||||||
static void queue(Context context, EntityMessage message, String name) {
|
static void queue(Context context, EntityMessage message, String name) {
|
||||||
JSONArray jsonArray = new JSONArray();
|
JSONArray jsonArray = new JSONArray();
|
||||||
|
|
|
@ -67,6 +67,14 @@ public class Helper {
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static String humanReadableByteCount(long bytes, boolean si) {
|
||||||
|
int unit = si ? 1000 : 1024;
|
||||||
|
if (bytes < unit) return bytes + " B";
|
||||||
|
int exp = (int) (Math.log(bytes) / Math.log(unit));
|
||||||
|
String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp - 1) + (si ? "" : "i");
|
||||||
|
return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre);
|
||||||
|
}
|
||||||
|
|
||||||
static StringBuilder getDebugInfo() {
|
static StringBuilder getDebugInfo() {
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
|
|
||||||
|
|
|
@ -313,6 +313,7 @@ public class MessageHelper {
|
||||||
attachment.sequence = result.size() + 1;
|
attachment.sequence = result.size() + 1;
|
||||||
attachment.name = part.getFileName();
|
attachment.name = part.getFileName();
|
||||||
attachment.type = ct.getBaseType();
|
attachment.type = ct.getBaseType();
|
||||||
|
attachment.part = part;
|
||||||
result.add(attachment);
|
result.add(attachment);
|
||||||
}
|
}
|
||||||
} else if (content instanceof Multipart) {
|
} else if (content instanceof Multipart) {
|
||||||
|
|
|
@ -50,7 +50,9 @@ import com.sun.mail.imap.protocol.IMAPProtocol;
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
@ -566,124 +568,151 @@ public class ServiceSynchronize extends LifecycleService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processOperations(EntityFolder folder, IMAPStore istore, IMAPFolder ifolder) throws MessagingException, JSONException {
|
private void processOperations(EntityFolder folder, IMAPStore istore, IMAPFolder ifolder) throws MessagingException, JSONException, IOException {
|
||||||
try {
|
try {
|
||||||
Log.i(Helper.TAG, folder.name + " start process");
|
Log.i(Helper.TAG, folder.name + " start process");
|
||||||
|
|
||||||
DB db = DB.getInstance(this);
|
DB db = DB.getInstance(this);
|
||||||
DaoOperation operation = db.operation();
|
DaoOperation operation = db.operation();
|
||||||
DaoMessage message = db.message();
|
DaoMessage message = db.message();
|
||||||
for (TupleOperationEx op : operation.getOperations(folder.id)) {
|
for (TupleOperationEx op : operation.getOperations(folder.id))
|
||||||
Log.i(Helper.TAG, folder.name +
|
|
||||||
" Process op=" + op.id + "/" + op.name +
|
|
||||||
" args=" + op.args +
|
|
||||||
" msg=" + op.message);
|
|
||||||
|
|
||||||
JSONArray jargs = new JSONArray(op.args);
|
|
||||||
try {
|
try {
|
||||||
if (EntityOperation.SEEN.equals(op.name)) {
|
Log.i(Helper.TAG, folder.name +
|
||||||
// Mark message (un)seen
|
" start op=" + op.id + "/" + op.name +
|
||||||
Message imessage = ifolder.getMessageByUID(op.uid);
|
" args=" + op.args +
|
||||||
if (imessage == null)
|
" msg=" + op.message);
|
||||||
throw new MessageRemovedException();
|
|
||||||
imessage.setFlag(Flags.Flag.SEEN, jargs.getBoolean(0));
|
|
||||||
|
|
||||||
} else if (EntityOperation.ADD.equals(op.name)) {
|
JSONArray jargs = new JSONArray(op.args);
|
||||||
if (!folder.synchronize) {
|
try {
|
||||||
// Local drafts
|
if (EntityOperation.SEEN.equals(op.name)) {
|
||||||
Log.w(Helper.TAG, "Folder synchronization disabled");
|
// Mark message (un)seen
|
||||||
return;
|
Message imessage = ifolder.getMessageByUID(op.uid);
|
||||||
}
|
if (imessage == null)
|
||||||
|
throw new MessageRemovedException();
|
||||||
|
imessage.setFlag(Flags.Flag.SEEN, jargs.getBoolean(0));
|
||||||
|
|
||||||
// Append message
|
} else if (EntityOperation.ADD.equals(op.name)) {
|
||||||
EntityMessage msg = message.getMessage(op.message);
|
if (!folder.synchronize) {
|
||||||
Properties props = MessageHelper.getSessionProperties();
|
// Local drafts
|
||||||
Session isession = Session.getDefaultInstance(props, null);
|
Log.w(Helper.TAG, "Folder synchronization disabled");
|
||||||
MimeMessage imessage = MessageHelper.from(msg, isession);
|
return;
|
||||||
ifolder.appendMessages(new Message[]{imessage});
|
|
||||||
|
|
||||||
// Drafts can be appended multiple times
|
|
||||||
try {
|
|
||||||
if (msg.uid != null) {
|
|
||||||
Message previously = ifolder.getMessageByUID(msg.uid);
|
|
||||||
previously.setFlag(Flags.Flag.DELETED, true);
|
|
||||||
ifolder.expunge();
|
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
// Remote will report appended
|
// Append message
|
||||||
|
EntityMessage msg = message.getMessage(op.message);
|
||||||
|
Properties props = MessageHelper.getSessionProperties();
|
||||||
|
Session isession = Session.getDefaultInstance(props, null);
|
||||||
|
MimeMessage imessage = MessageHelper.from(msg, isession);
|
||||||
|
ifolder.appendMessages(new Message[]{imessage});
|
||||||
|
|
||||||
|
// Drafts can be appended multiple times
|
||||||
|
try {
|
||||||
|
if (msg.uid != null) {
|
||||||
|
Message previously = ifolder.getMessageByUID(msg.uid);
|
||||||
|
previously.setFlag(Flags.Flag.DELETED, true);
|
||||||
|
ifolder.expunge();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// Remote will report appended
|
||||||
|
message.deleteMessage(op.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (EntityOperation.MOVE.equals(op.name)) {
|
||||||
|
// Move message
|
||||||
|
EntityFolder archive = db.folder().getFolder(jargs.getLong(0));
|
||||||
|
Message imessage = ifolder.getMessageByUID(op.uid);
|
||||||
|
Folder target = istore.getFolder(archive.name);
|
||||||
|
ifolder.moveMessages(new Message[]{imessage}, target);
|
||||||
|
|
||||||
message.deleteMessage(op.message);
|
message.deleteMessage(op.message);
|
||||||
}
|
|
||||||
|
|
||||||
} else if (EntityOperation.MOVE.equals(op.name)) {
|
} else if (EntityOperation.DELETE.equals(op.name)) {
|
||||||
// Move message
|
// Delete message
|
||||||
EntityFolder archive = db.folder().getFolder(jargs.getLong(0));
|
Message imessage = ifolder.getMessageByUID(op.uid);
|
||||||
Message imessage = ifolder.getMessageByUID(op.uid);
|
if (imessage == null)
|
||||||
Folder target = istore.getFolder(archive.name);
|
throw new MessageRemovedException();
|
||||||
ifolder.moveMessages(new Message[]{imessage}, target);
|
imessage.setFlag(Flags.Flag.DELETED, true);
|
||||||
|
ifolder.expunge();
|
||||||
|
|
||||||
message.deleteMessage(op.message);
|
|
||||||
|
|
||||||
} else if (EntityOperation.DELETE.equals(op.name)) {
|
|
||||||
// Delete message
|
|
||||||
Message imessage = ifolder.getMessageByUID(op.uid);
|
|
||||||
if (imessage == null)
|
|
||||||
throw new MessageRemovedException();
|
|
||||||
imessage.setFlag(Flags.Flag.DELETED, true);
|
|
||||||
ifolder.expunge();
|
|
||||||
|
|
||||||
message.deleteMessage(op.message);
|
|
||||||
|
|
||||||
} else if (EntityOperation.SEND.equals(op.name)) {
|
|
||||||
// Send message
|
|
||||||
EntityMessage msg = message.getMessage(op.message);
|
|
||||||
EntityMessage reply = (msg.replying == null ? null : message.getMessage(msg.replying));
|
|
||||||
EntityIdentity ident = db.identity().getIdentity(msg.identity);
|
|
||||||
|
|
||||||
if (!ident.synchronize) {
|
|
||||||
// Message will remain in outbox
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Properties props = MessageHelper.getSessionProperties();
|
|
||||||
Session isession = Session.getDefaultInstance(props, null);
|
|
||||||
|
|
||||||
MimeMessage imessage;
|
|
||||||
if (reply == null)
|
|
||||||
imessage = MessageHelper.from(msg, isession);
|
|
||||||
else
|
|
||||||
imessage = MessageHelper.from(msg, reply, isession);
|
|
||||||
if (ident.replyto != null)
|
|
||||||
imessage.setReplyTo(new Address[]{new InternetAddress(ident.replyto)});
|
|
||||||
|
|
||||||
Transport itransport = isession.getTransport(ident.starttls ? "smtp" : "smtps");
|
|
||||||
try {
|
|
||||||
itransport.connect(ident.host, ident.port, ident.user, ident.password);
|
|
||||||
|
|
||||||
Address[] to = imessage.getAllRecipients();
|
|
||||||
itransport.sendMessage(imessage, to);
|
|
||||||
Log.i(Helper.TAG, "Sent via " + ident.host + "/" + ident.user +
|
|
||||||
" to " + TextUtils.join(", ", to));
|
|
||||||
|
|
||||||
// Make sure the message is sent only once
|
|
||||||
operation.deleteOperation(op.id);
|
|
||||||
message.deleteMessage(op.message);
|
message.deleteMessage(op.message);
|
||||||
} finally {
|
|
||||||
itransport.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
} else
|
} else if (EntityOperation.SEND.equals(op.name)) {
|
||||||
throw new MessagingException("Unknown operation name=" + op.name);
|
// Send message
|
||||||
|
EntityMessage msg = message.getMessage(op.message);
|
||||||
|
EntityMessage reply = (msg.replying == null ? null : message.getMessage(msg.replying));
|
||||||
|
EntityIdentity ident = db.identity().getIdentity(msg.identity);
|
||||||
|
|
||||||
// Operation succeeded
|
if (!ident.synchronize) {
|
||||||
operation.deleteOperation(op.id);
|
// Message will remain in outbox
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
} catch (MessageRemovedException ex) {
|
Properties props = MessageHelper.getSessionProperties();
|
||||||
Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
|
Session isession = Session.getDefaultInstance(props, null);
|
||||||
|
|
||||||
// There is no use in repeating
|
MimeMessage imessage;
|
||||||
operation.deleteOperation(op.id);
|
if (reply == null)
|
||||||
|
imessage = MessageHelper.from(msg, isession);
|
||||||
|
else
|
||||||
|
imessage = MessageHelper.from(msg, reply, isession);
|
||||||
|
if (ident.replyto != null)
|
||||||
|
imessage.setReplyTo(new Address[]{new InternetAddress(ident.replyto)});
|
||||||
|
|
||||||
|
Transport itransport = isession.getTransport(ident.starttls ? "smtp" : "smtps");
|
||||||
|
try {
|
||||||
|
itransport.connect(ident.host, ident.port, ident.user, ident.password);
|
||||||
|
|
||||||
|
Address[] to = imessage.getAllRecipients();
|
||||||
|
itransport.sendMessage(imessage, to);
|
||||||
|
Log.i(Helper.TAG, "Sent via " + ident.host + "/" + ident.user +
|
||||||
|
" to " + TextUtils.join(", ", to));
|
||||||
|
|
||||||
|
// Make sure the message is sent only once
|
||||||
|
operation.deleteOperation(op.id);
|
||||||
|
message.deleteMessage(op.message);
|
||||||
|
} finally {
|
||||||
|
itransport.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (EntityOperation.ATTACHMENT.equals(op.name)) {
|
||||||
|
int sequence = jargs.getInt(0);
|
||||||
|
EntityAttachment attachment = db.attachment().getAttachment(op.message, sequence);
|
||||||
|
|
||||||
|
Message imessage = ifolder.getMessageByUID(op.uid);
|
||||||
|
if (imessage == null)
|
||||||
|
throw new MessageRemovedException();
|
||||||
|
|
||||||
|
Properties props = MessageHelper.getSessionProperties();
|
||||||
|
Session isession = Session.getDefaultInstance(props, null);
|
||||||
|
|
||||||
|
MessageHelper helper = new MessageHelper((MimeMessage) imessage);
|
||||||
|
EntityAttachment a = helper.getAttachments().get(sequence - 1);
|
||||||
|
|
||||||
|
InputStream is = a.part.getInputStream();
|
||||||
|
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||||
|
byte[] buffer = new byte[4096];
|
||||||
|
for (int len = is.read(buffer); len != -1; len = is.read(buffer))
|
||||||
|
os.write(buffer, 0, len);
|
||||||
|
|
||||||
|
attachment.content = os.toByteArray();
|
||||||
|
db.attachment().updateAttachment(attachment);
|
||||||
|
Log.i(Helper.TAG, "Downloaded bytes=" + attachment.content.length);
|
||||||
|
|
||||||
|
} else
|
||||||
|
throw new MessagingException("Unknown operation name=" + op.name);
|
||||||
|
|
||||||
|
// Operation succeeded
|
||||||
|
operation.deleteOperation(op.id);
|
||||||
|
|
||||||
|
} catch (MessageRemovedException ex) {
|
||||||
|
Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
|
||||||
|
|
||||||
|
// There is no use in repeating
|
||||||
|
operation.deleteOperation(op.id);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
Log.i(Helper.TAG, folder.name + " end op=" + op.id + "/" + op.name);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
Log.i(Helper.TAG, folder.name + " end process");
|
Log.i(Helper.TAG, folder.name + " end process");
|
||||||
}
|
}
|
||||||
|
|
After Width: | Height: | Size: 187 B |
After Width: | Height: | Size: 164 B |
After Width: | Height: | Size: 207 B |
After Width: | Height: | Size: 185 B |
After Width: | Height: | Size: 190 B |
After Width: | Height: | Size: 167 B |
After Width: | Height: | Size: 211 B |
After Width: | Height: | Size: 186 B |
After Width: | Height: | Size: 143 B |
After Width: | Height: | Size: 121 B |
After Width: | Height: | Size: 164 B |
After Width: | Height: | Size: 159 B |
After Width: | Height: | Size: 146 B |
After Width: | Height: | Size: 121 B |
After Width: | Height: | Size: 167 B |
After Width: | Height: | Size: 161 B |
After Width: | Height: | Size: 164 B |
After Width: | Height: | Size: 159 B |
After Width: | Height: | Size: 185 B |
After Width: | Height: | Size: 208 B |
After Width: | Height: | Size: 167 B |
After Width: | Height: | Size: 161 B |
After Width: | Height: | Size: 186 B |
After Width: | Height: | Size: 208 B |
After Width: | Height: | Size: 207 B |
After Width: | Height: | Size: 185 B |
After Width: | Height: | Size: 261 B |
After Width: | Height: | Size: 263 B |
After Width: | Height: | Size: 211 B |
After Width: | Height: | Size: 186 B |
After Width: | Height: | Size: 261 B |
After Width: | Height: | Size: 263 B |
After Width: | Height: | Size: 185 B |
After Width: | Height: | Size: 208 B |
After Width: | Height: | Size: 263 B |
After Width: | Height: | Size: 303 B |
After Width: | Height: | Size: 186 B |
After Width: | Height: | Size: 208 B |
After Width: | Height: | Size: 263 B |
After Width: | Height: | Size: 303 B |
|
@ -0,0 +1,10 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z"/>
|
||||||
|
</vector>
|
|
@ -16,25 +16,38 @@
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tvName"
|
android:id="@+id/tvName"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="6dp"
|
android:layout_marginStart="6dp"
|
||||||
|
android:ellipsize="marquee"
|
||||||
|
android:singleLine="true"
|
||||||
android:text="Name"
|
android:text="Name"
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/ivAttachments"
|
app:layout_constraintBottom_toBottomOf="@id/ivAttachments"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/tvSize"
|
||||||
app:layout_constraintStart_toEndOf="@id/ivAttachments"
|
app:layout_constraintStart_toEndOf="@id/ivAttachments"
|
||||||
app:layout_constraintTop_toTopOf="@id/ivAttachments" />
|
app:layout_constraintTop_toTopOf="@id/ivAttachments" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tvType"
|
android:id="@+id/tvSize"
|
||||||
android:layout_width="0dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="6dp"
|
android:layout_marginStart="6dp"
|
||||||
android:gravity="end"
|
android:text="10 kB"
|
||||||
android:text="Type"
|
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/ivAttachments"
|
app:layout_constraintBottom_toBottomOf="@id/ivAttachments"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toStartOf="@+id/ivDownload"
|
||||||
app:layout_constraintStart_toEndOf="@id/tvName"
|
app:layout_constraintStart_toEndOf="@id/tvName"
|
||||||
app:layout_constraintTop_toTopOf="@id/ivAttachments" />
|
app:layout_constraintTop_toTopOf="@id/ivAttachments" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/ivDownload"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_marginEnd="6dp"
|
||||||
|
android:layout_marginStart="6dp"
|
||||||
|
android:src="@drawable/baseline_get_app_24"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/tvSize"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/ivAttachments" />
|
||||||
</android.support.constraint.ConstraintLayout>
|
</android.support.constraint.ConstraintLayout>
|