Added attachment size/progress

This commit is contained in:
M66B 2018-08-04 10:32:34 +00:00
parent e5a475379b
commit 0c53244342
7 changed files with 760 additions and 35 deletions

View File

@ -0,0 +1,651 @@
{
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "40fd6ebf37a522fd68104290c7f30208",
"entities": [
{
"tableName": "identity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `email` TEXT NOT NULL, `replyto` TEXT, `host` TEXT NOT NULL, `port` INTEGER NOT NULL, `starttls` INTEGER NOT NULL, `user` TEXT NOT NULL, `password` TEXT NOT NULL, `primary` INTEGER NOT NULL, `synchronize` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "email",
"columnName": "email",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "replyto",
"columnName": "replyto",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "host",
"columnName": "host",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "port",
"columnName": "port",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "starttls",
"columnName": "starttls",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "user",
"columnName": "user",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "password",
"columnName": "password",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "primary",
"columnName": "primary",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "synchronize",
"columnName": "synchronize",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "account",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT, `host` TEXT NOT NULL, `port` INTEGER NOT NULL, `user` TEXT NOT NULL, `password` TEXT NOT NULL, `primary` INTEGER NOT NULL, `synchronize` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "host",
"columnName": "host",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "port",
"columnName": "port",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "user",
"columnName": "user",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "password",
"columnName": "password",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "primary",
"columnName": "primary",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "synchronize",
"columnName": "synchronize",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "folder",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `account` INTEGER, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `synchronize` INTEGER NOT NULL, `after` INTEGER NOT NULL, FOREIGN KEY(`account`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "account",
"columnName": "account",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "synchronize",
"columnName": "synchronize",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "after",
"columnName": "after",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_folder_account_name",
"unique": true,
"columnNames": [
"account",
"name"
],
"createSql": "CREATE UNIQUE INDEX `index_folder_account_name` ON `${TABLE_NAME}` (`account`, `name`)"
},
{
"name": "index_folder_account",
"unique": false,
"columnNames": [
"account"
],
"createSql": "CREATE INDEX `index_folder_account` ON `${TABLE_NAME}` (`account`)"
},
{
"name": "index_folder_name",
"unique": false,
"columnNames": [
"name"
],
"createSql": "CREATE INDEX `index_folder_name` ON `${TABLE_NAME}` (`name`)"
},
{
"name": "index_folder_type",
"unique": false,
"columnNames": [
"type"
],
"createSql": "CREATE INDEX `index_folder_type` ON `${TABLE_NAME}` (`type`)"
}
],
"foreignKeys": [
{
"table": "account",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"account"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "message",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `account` INTEGER, `folder` INTEGER NOT NULL, `identity` INTEGER, `replying` INTEGER, `uid` INTEGER, `msgid` TEXT, `references` TEXT, `inreplyto` TEXT, `thread` TEXT, `from` TEXT, `to` TEXT, `cc` TEXT, `bcc` TEXT, `reply` TEXT, `subject` TEXT, `body` TEXT, `sent` INTEGER, `received` INTEGER NOT NULL, `seen` INTEGER NOT NULL, `ui_seen` INTEGER NOT NULL, `ui_hide` INTEGER NOT NULL, FOREIGN KEY(`account`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`folder`) REFERENCES `folder`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`identity`) REFERENCES `identity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`replying`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "account",
"columnName": "account",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "folder",
"columnName": "folder",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "identity",
"columnName": "identity",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "replying",
"columnName": "replying",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "msgid",
"columnName": "msgid",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "references",
"columnName": "references",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "inreplyto",
"columnName": "inreplyto",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thread",
"columnName": "thread",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "from",
"columnName": "from",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "to",
"columnName": "to",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "cc",
"columnName": "cc",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "bcc",
"columnName": "bcc",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "reply",
"columnName": "reply",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "subject",
"columnName": "subject",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "body",
"columnName": "body",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sent",
"columnName": "sent",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "received",
"columnName": "received",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "seen",
"columnName": "seen",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ui_seen",
"columnName": "ui_seen",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ui_hide",
"columnName": "ui_hide",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_message_account",
"unique": false,
"columnNames": [
"account"
],
"createSql": "CREATE INDEX `index_message_account` ON `${TABLE_NAME}` (`account`)"
},
{
"name": "index_message_folder",
"unique": false,
"columnNames": [
"folder"
],
"createSql": "CREATE INDEX `index_message_folder` ON `${TABLE_NAME}` (`folder`)"
},
{
"name": "index_message_identity",
"unique": false,
"columnNames": [
"identity"
],
"createSql": "CREATE INDEX `index_message_identity` ON `${TABLE_NAME}` (`identity`)"
},
{
"name": "index_message_replying",
"unique": false,
"columnNames": [
"replying"
],
"createSql": "CREATE INDEX `index_message_replying` ON `${TABLE_NAME}` (`replying`)"
},
{
"name": "index_message_folder_uid",
"unique": true,
"columnNames": [
"folder",
"uid"
],
"createSql": "CREATE UNIQUE INDEX `index_message_folder_uid` ON `${TABLE_NAME}` (`folder`, `uid`)"
},
{
"name": "index_message_thread",
"unique": false,
"columnNames": [
"thread"
],
"createSql": "CREATE INDEX `index_message_thread` ON `${TABLE_NAME}` (`thread`)"
},
{
"name": "index_message_received",
"unique": false,
"columnNames": [
"received"
],
"createSql": "CREATE INDEX `index_message_received` ON `${TABLE_NAME}` (`received`)"
}
],
"foreignKeys": [
{
"table": "account",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"account"
],
"referencedColumns": [
"id"
]
},
{
"table": "folder",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"folder"
],
"referencedColumns": [
"id"
]
},
{
"table": "identity",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"identity"
],
"referencedColumns": [
"id"
]
},
{
"table": "message",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"replying"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "attachment",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `message` INTEGER NOT NULL, `sequence` INTEGER NOT NULL, `name` TEXT, `type` TEXT NOT NULL, `size` INTEGER, `progress` INTEGER, `content` BLOB, FOREIGN KEY(`message`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "message",
"columnName": "message",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "sequence",
"columnName": "sequence",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "progress",
"columnName": "progress",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "content",
"columnName": "content",
"affinity": "BLOB",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_attachment_message",
"unique": false,
"columnNames": [
"message"
],
"createSql": "CREATE INDEX `index_attachment_message` ON `${TABLE_NAME}` (`message`)"
},
{
"name": "index_attachment_message_sequence",
"unique": true,
"columnNames": [
"message",
"sequence"
],
"createSql": "CREATE UNIQUE INDEX `index_attachment_message_sequence` ON `${TABLE_NAME}` (`message`, `sequence`)"
}
],
"foreignKeys": [
{
"table": "message",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"message"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "operation",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `message` INTEGER NOT NULL, `name` TEXT NOT NULL, `args` TEXT, FOREIGN KEY(`message`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "message",
"columnName": "message",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "args",
"columnName": "args",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_operation_message",
"unique": false,
"columnNames": [
"message"
],
"createSql": "CREATE INDEX `index_operation_message` ON `${TABLE_NAME}` (`message`)"
}
],
"foreignKeys": [
{
"table": "message",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"message"
],
"referencedColumns": [
"id"
]
}
]
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"40fd6ebf37a522fd68104290c7f30208\")"
]
}
}

View File

@ -50,7 +50,8 @@ public class AdapterAttachment extends RecyclerView.Adapter<AdapterAttachment.Vi
View itemView;
TextView tvName;
TextView tvSize;
ImageView ivDownload;
TextView tvProgress;
ImageView ivStatus;
ViewHolder(View itemView) {
super(itemView);
@ -58,30 +59,37 @@ public class AdapterAttachment extends RecyclerView.Adapter<AdapterAttachment.Vi
this.itemView = itemView;
tvName = itemView.findViewById(R.id.tvName);
tvSize = itemView.findViewById(R.id.tvSize);
ivDownload = itemView.findViewById(R.id.ivDownload);
tvProgress = itemView.findViewById(R.id.tvProgress);
ivStatus = itemView.findViewById(R.id.ivStatus);
}
private void wire() {
itemView.setOnClickListener(this);
ivDownload.setOnClickListener(this);
}
private void unwire() {
itemView.setOnClickListener(null);
ivDownload.setOnClickListener(null);
}
@Override
public void onClick(View view) {
final EntityAttachment attachment = filtered.get(getLayoutPosition());
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);
}
});
if (attachment != null)
if (attachment.content == null) {
if (attachment.progress == null)
executor.submit(new Runnable() {
@Override
public void run() {
DB db = DB.getInstance(context);
attachment.progress = 0;
db.attachment().updateAttachment(attachment);
EntityMessage message = db.message().getMessage(attachment.message);
EntityOperation.queue(context, message, EntityOperation.ATTACHMENT, attachment.sequence);
}
});
} else {
// View
}
}
}
@ -188,11 +196,25 @@ public class AdapterAttachment extends RecyclerView.Adapter<AdapterAttachment.Vi
EntityAttachment attachment = filtered.get(position);
holder.tvName.setText(attachment.name);
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));
if (attachment.size != null)
holder.tvSize.setText(Helper.humanReadableByteCount(attachment.size, false));
holder.tvSize.setVisibility(attachment.size == null ? View.GONE : View.VISIBLE);
if (attachment.progress != null)
holder.tvProgress.setText(String.format("%d %%", attachment.progress));
holder.tvProgress.setVisibility(attachment.progress == null ? View.GONE : View.VISIBLE);
if (attachment.content == null) {
if (attachment.progress == null) {
holder.ivStatus.setImageResource(R.drawable.baseline_get_app_24);
holder.ivStatus.setVisibility(View.VISIBLE);
} else
holder.ivStatus.setVisibility(View.GONE);
} else {
holder.ivStatus.setImageResource(R.drawable.baseline_visibility_24);
holder.ivStatus.setVisibility(View.VISIBLE);
}
holder.wire();
}

View File

@ -40,7 +40,7 @@ import android.util.Log;
EntityAttachment.class,
EntityOperation.class
},
version = 2,
version = 3,
exportSchema = true
)
@ -75,6 +75,7 @@ public abstract class DB extends RoomDatabase {
private static DB migrate(RoomDatabase.Builder<DB> builder) {
return builder
.addMigrations(MIGRATION_1_2)
.addMigrations(MIGRATION_2_3)
.build();
}
@ -93,6 +94,15 @@ public abstract class DB extends RoomDatabase {
}
};
private static final Migration MIGRATION_2_3 = new Migration(2, 3) {
@Override
public void migrate(SupportSQLiteDatabase db) {
Log.i(Helper.TAG, "DB migration from version " + startVersion + " to " + endVersion);
db.execSQL("ALTER TABLE `attachment` ADD COLUMN `size` INTEGER");
db.execSQL("ALTER TABLE `attachment` ADD COLUMN `progress` INTEGER");
}
};
public static class Converters {
@TypeConverter
public static byte[] fromString(String value) {

View File

@ -52,6 +52,8 @@ public class EntityAttachment {
public String name;
@NonNull
public String type;
public Integer size;
public Integer progress;
public byte[] content;
@Ignore

View File

@ -313,7 +313,10 @@ public class MessageHelper {
attachment.sequence = result.size() + 1;
attachment.name = part.getFileName();
attachment.type = ct.getBaseType();
attachment.size = part.getSize();
attachment.part = part;
if (attachment.size < 0)
attachment.size = null;
result.add(attachment);
}
} else if (content instanceof Multipart) {

View File

@ -96,6 +96,7 @@ public class ServiceSynchronize extends LifecycleService {
private static final long NOOP_INTERVAL = 9 * 60 * 1000L; // ms
private static final int FETCH_BATCH_SIZE = 10;
private static final int DOWNLOAD_BUFFER_SIZE = 8192; // bytes
static final String ACTION_PROCESS_OPERATIONS = BuildConfig.APPLICATION_ID + ".PROCESS_OPERATIONS.";
@ -699,23 +700,47 @@ public class ServiceSynchronize extends LifecycleService {
if (attachment == null)
return;
Message imessage = ifolder.getMessageByUID(op.uid);
if (imessage == null)
throw new MessageRemovedException();
try {
// Get message
Message imessage = ifolder.getMessageByUID(op.uid);
if (imessage == null)
throw new MessageRemovedException();
MessageHelper helper = new MessageHelper((MimeMessage) imessage);
EntityAttachment a = helper.getAttachments().get(sequence - 1);
// Get attachment
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);
// Download attachment
InputStream is = a.part.getInputStream();
ByteArrayOutputStream os = new ByteArrayOutputStream();
byte[] buffer = new byte[DOWNLOAD_BUFFER_SIZE];
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);
// Update progress
if (attachment.size != null) {
attachment.progress = os.size() * 100 / attachment.size;
db.attachment().updateAttachment(attachment);
Log.i(Helper.TAG, "Progress %=" + attachment.progress);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// Store attachment data
attachment.progress = 100;
attachment.content = os.toByteArray();
db.attachment().updateAttachment(attachment);
Log.i(Helper.TAG, "Downloaded bytes=" + attachment.content.length);
} catch (Throwable ex) {
// Reset progress on failure
attachment.progress = null;
db.attachment().updateAttachment(attachment);
throw ex;
}
} else
throw new MessagingException("Unknown operation name=" + op.name);

View File

@ -11,6 +11,7 @@
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/baseline_attachment_24"
app:layout_constraintEnd_toStartOf="@+id/tvName"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
@ -35,18 +36,29 @@
android:text="10 kB"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintBottom_toBottomOf="@id/ivAttachments"
app:layout_constraintEnd_toStartOf="@+id/ivDownload"
app:layout_constraintEnd_toStartOf="@+id/tvProgress"
app:layout_constraintStart_toEndOf="@id/tvName"
app:layout_constraintTop_toTopOf="@id/ivAttachments" />
<TextView
android:id="@+id/tvProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:text="50 %"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintBottom_toBottomOf="@id/ivAttachments"
app:layout_constraintEnd_toStartOf="@+id/ivStatus"
app:layout_constraintStart_toEndOf="@id/tvSize"
app:layout_constraintTop_toTopOf="@id/ivAttachments" />
<ImageView
android:id="@+id/ivDownload"
android:id="@+id/ivStatus"
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" />
app:layout_constraintStart_toEndOf="@id/tvProgress"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>