Arbitrary attachments

This commit is contained in:
Philipp Heckel 2022-02-11 20:34:08 -05:00
parent 8100e68b8d
commit 9afdf5e6e7
9 changed files with 134 additions and 53 deletions

View file

@ -29,7 +29,7 @@ class ApiService {
.build() .build()
private val parser = NotificationParser() private val parser = NotificationParser()
fun publish(baseUrl: String, topic: String, user: User?, message: String, title: String, priority: Int, tags: List<String>, delay: String, body: RequestBody? = null) { fun publish(baseUrl: String, topic: String, user: User?, message: String, title: String, priority: Int, tags: List<String>, delay: String, body: RequestBody? = null, filename: String = "") {
val url = topicUrl(baseUrl, topic) val url = topicUrl(baseUrl, topic)
Log.d(TAG, "Publishing to $url") Log.d(TAG, "Publishing to $url")
@ -46,6 +46,9 @@ class ApiService {
if (delay.isNotEmpty()) { if (delay.isNotEmpty()) {
builder.addHeader("X-Delay", delay) builder.addHeader("X-Delay", delay)
} }
if (filename.isNotEmpty()) {
builder.addHeader("X-Filename", filename)
}
if (body != null) { if (body != null) {
builder builder
.addHeader("X-Message", message) .addHeader("X-Message", message)

View file

@ -18,7 +18,7 @@ import io.heckel.ntfy.R
import io.heckel.ntfy.app.Application import io.heckel.ntfy.app.Application
import io.heckel.ntfy.db.* import io.heckel.ntfy.db.*
import io.heckel.ntfy.util.Log import io.heckel.ntfy.util.Log
import io.heckel.ntfy.util.queryFilename import io.heckel.ntfy.util.fileName
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
@ -132,7 +132,7 @@ class DownloadWorker(private val context: Context, params: WorkerParameters) : W
} }
} }
Log.d(TAG, "Attachment download: successful response, proceeding with download") Log.d(TAG, "Attachment download: successful response, proceeding with download")
val actualName = queryFilename(context, uri.toString(), attachment.name) val actualName = fileName(context, uri.toString(), attachment.name)
save(attachment.copy( save(attachment.copy(
name = actualName, name = actualName,
size = bytesCopied, size = bytesCopied,

View file

@ -110,7 +110,6 @@ class AddFragment : DialogFragment() {
endIconImageView.minimumWidth = dimension.toInt() endIconImageView.minimumWidth = dimension.toInt()
subscribeBaseUrlLayout.requestLayout() subscribeBaseUrlLayout.requestLayout()
// Fields for "login page" // Fields for "login page"
loginUsernameText = view.findViewById(R.id.add_dialog_login_username) loginUsernameText = view.findViewById(R.id.add_dialog_login_username)
loginPasswordText = view.findViewById(R.id.add_dialog_login_password) loginPasswordText = view.findViewById(R.id.add_dialog_login_password)

View file

@ -68,9 +68,9 @@ class DetailAdapter(private val activity: Activity, private val repository: Repo
private val tagsView: TextView = itemView.findViewById(R.id.detail_item_tags_text) private val tagsView: TextView = itemView.findViewById(R.id.detail_item_tags_text)
private val menuButton: ImageButton = itemView.findViewById(R.id.detail_item_menu_button) private val menuButton: ImageButton = itemView.findViewById(R.id.detail_item_menu_button)
private val attachmentImageView: ImageView = itemView.findViewById(R.id.detail_item_attachment_image) private val attachmentImageView: ImageView = itemView.findViewById(R.id.detail_item_attachment_image)
private val attachmentBoxView: View = itemView.findViewById(R.id.detail_item_attachment_box) private val attachmentBoxView: View = itemView.findViewById(R.id.share_content_file_box)
private val attachmentIconView: ImageView = itemView.findViewById(R.id.detail_item_attachment_icon) private val attachmentIconView: ImageView = itemView.findViewById(R.id.share_content_file_icon)
private val attachmentInfoView: TextView = itemView.findViewById(R.id.detail_item_attachment_info) private val attachmentInfoView: TextView = itemView.findViewById(R.id.share_content_file_info)
fun bind(notification: Notification) { fun bind(notification: Notification) {
this.notification = notification this.notification = notification
@ -157,17 +157,7 @@ class DetailAdapter(private val activity: Activity, private val repository: Repo
return return
} }
attachmentInfoView.text = formatAttachmentDetails(context, attachment, exists) attachmentInfoView.text = formatAttachmentDetails(context, attachment, exists)
attachmentIconView.setImageResource(if (attachment.type?.startsWith("image/") == true) { attachmentIconView.setImageResource(mimeTypeToIconResource(attachment.type))
R.drawable.ic_file_image_red_24dp
} else if (attachment.type?.startsWith("video/") == true) {
R.drawable.ic_file_video_orange_24dp
} else if (attachment.type?.startsWith("audio/") == true) {
R.drawable.ic_file_audio_purple_24dp
} else if ("application/vnd.android.package-archive" == attachment.type) {
R.drawable.ic_file_app_gray_24dp
} else {
R.drawable.ic_file_document_blue_24dp
})
val attachmentBoxPopupMenu = createAttachmentPopup(context, attachmentBoxView, notification, attachment, exists) // Heavy lifting not during on-click val attachmentBoxPopupMenu = createAttachmentPopup(context, attachmentBoxView, notification, attachment, exists) // Heavy lifting not during on-click
if (attachmentBoxPopupMenu != null) { if (attachmentBoxPopupMenu != null) {
attachmentBoxView.setOnClickListener { attachmentBoxPopupMenu.show() } attachmentBoxView.setOnClickListener { attachmentBoxPopupMenu.show() }
@ -275,7 +265,7 @@ class DetailAdapter(private val activity: Activity, private val repository: Repo
} }
private fun formatAttachmentDetails(context: Context, attachment: Attachment, exists: Boolean): String { private fun formatAttachmentDetails(context: Context, attachment: Attachment, exists: Boolean): String {
val name = queryFilename(context, attachment.contentUri, attachment.name) val name = fileName(context, attachment.contentUri, attachment.name)
val notYetDownloaded = !exists && attachment.progress == PROGRESS_NONE val notYetDownloaded = !exists && attachment.progress == PROGRESS_NONE
val downloading = !exists && attachment.progress in 0..99 val downloading = !exists && attachment.progress in 0..99
val deleted = !exists && (attachment.progress == PROGRESS_DONE || attachment.progress == PROGRESS_DELETED) val deleted = !exists && (attachment.progress == PROGRESS_DONE || attachment.progress == PROGRESS_DELETED)

View file

@ -13,14 +13,13 @@ import android.view.View
import android.widget.ImageView import android.widget.ImageView
import android.widget.ProgressBar import android.widget.ProgressBar
import android.widget.TextView import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import io.heckel.ntfy.R import io.heckel.ntfy.R
import io.heckel.ntfy.app.Application import io.heckel.ntfy.app.Application
import io.heckel.ntfy.msg.ApiService import io.heckel.ntfy.msg.ApiService
import io.heckel.ntfy.util.ContentUriRequestBody import io.heckel.ntfy.util.*
import io.heckel.ntfy.util.Log
import io.heckel.ntfy.util.supportedImage
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -35,6 +34,9 @@ class ShareActivity : AppCompatActivity() {
private lateinit var menu: Menu private lateinit var menu: Menu
private lateinit var sendItem: MenuItem private lateinit var sendItem: MenuItem
private lateinit var contentImage: ImageView private lateinit var contentImage: ImageView
private lateinit var contentFileBox: View
private lateinit var contentFileInfo: TextView
private lateinit var contentFileIcon: ImageView
private lateinit var contentText: TextView private lateinit var contentText: TextView
private lateinit var topicText: TextView private lateinit var topicText: TextView
private lateinit var progress: ProgressBar private lateinit var progress: ProgressBar
@ -57,6 +59,9 @@ class ShareActivity : AppCompatActivity() {
// UI elements // UI elements
contentText = findViewById(R.id.share_content_text) contentText = findViewById(R.id.share_content_text)
contentImage = findViewById(R.id.share_content_image) contentImage = findViewById(R.id.share_content_image)
contentFileBox = findViewById(R.id.share_content_file_box)
contentFileInfo = findViewById(R.id.share_content_file_info)
contentFileIcon = findViewById(R.id.share_content_file_icon)
topicText = findViewById(R.id.share_topic_text) topicText = findViewById(R.id.share_topic_text)
progress = findViewById(R.id.share_progress) progress = findViewById(R.id.share_progress)
progress.visibility = View.GONE progress.visibility = View.GONE
@ -93,8 +98,8 @@ class ShareActivity : AppCompatActivity() {
private fun handleSendText(intent: Intent) { private fun handleSendText(intent: Intent) {
intent.getStringExtra(Intent.EXTRA_TEXT)?.let { text -> intent.getStringExtra(Intent.EXTRA_TEXT)?.let { text ->
contentImage.visibility = View.GONE
contentText.text = text contentText.text = text
show()
} }
} }
@ -105,18 +110,33 @@ class ShareActivity : AppCompatActivity() {
val bitmapStream = resolver.openInputStream(fileUri!!) val bitmapStream = resolver.openInputStream(fileUri!!)
val bitmap = BitmapFactory.decodeStream(bitmapStream) val bitmap = BitmapFactory.decodeStream(bitmapStream)
contentImage.setImageBitmap(bitmap) contentImage.setImageBitmap(bitmap)
contentImage.visibility = View.VISIBLE
contentText.text = getString(R.string.share_content_image_text) contentText.text = getString(R.string.share_content_image_text)
} catch (_: Exception) { show(image = true)
} catch (e: Exception) {
fileUri = null fileUri = null
contentImage.visibility = View.GONE contentText.text = ""
contentText.text = getString(R.string.share_content_image_error) errorText.text = getString(R.string.share_content_image_error, e.message)
show(error = true)
} }
} }
private fun handleSendFile(intent: Intent) { private fun handleSendFile(intent: Intent) {
fileUri = intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri ?: return fileUri = intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri ?: return
contentText.text = getString(R.string.share_content_file_text) try {
val resolver = applicationContext.contentResolver
val info = fileStat(this, fileUri)
val mimeType = resolver.getType(fileUri!!)
contentText.text = getString(R.string.share_content_file_text)
contentFileInfo.text = "${info.filename}\n${formatBytes(info.size)}"
contentFileIcon.setImageResource(mimeTypeToIconResource(mimeType))
show(file = true)
} catch (e: Exception) {
fileUri = null
contentText.text = ""
errorText.text = getString(R.string.share_content_file_error, e.message)
show(error = true)
}
} }
override fun onSupportNavigateUp(): Boolean { override fun onSupportNavigateUp(): Boolean {
@ -142,6 +162,13 @@ class ShareActivity : AppCompatActivity() {
} }
} }
private fun show(image: Boolean = false, file: Boolean = false, error: Boolean = false) {
contentImage.visibility = if (image) View.VISIBLE else View.GONE
contentFileBox.visibility = if (file) View.VISIBLE else View.GONE
errorImage.visibility = if (error) View.VISIBLE else View.GONE
errorText.visibility = if (error) View.VISIBLE else View.GONE
}
private fun onShareClick() { private fun onShareClick() {
val baseUrl = "https://ntfy.sh" // FIXME val baseUrl = "https://ntfy.sh" // FIXME
val topic = topicText.text.toString() val topic = topicText.text.toString()
@ -150,6 +177,11 @@ class ShareActivity : AppCompatActivity() {
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
val user = repository.getUser(baseUrl) val user = repository.getUser(baseUrl)
try { try {
val filename = if (fileUri != null) {
fileStat(this@ShareActivity, fileUri).filename
} else {
""
}
val body = if (fileUri != null) { val body = if (fileUri != null) {
val resolver = applicationContext.contentResolver val resolver = applicationContext.contentResolver
ContentUriRequestBody(resolver, fileUri!!) ContentUriRequestBody(resolver, fileUri!!)
@ -165,10 +197,14 @@ class ShareActivity : AppCompatActivity() {
priority = 3, priority = 3,
tags = emptyList(), tags = emptyList(),
delay = "", delay = "",
body = body // May be null body = body, // May be null
filename = filename // May be empty
) )
runOnUiThread { runOnUiThread {
finish() finish()
Toast
.makeText(this@ShareActivity, getString(R.string.share_successful), Toast.LENGTH_LONG)
.show()
} }
} catch (e: Exception) { } catch (e: Exception) {
runOnUiThread { runOnUiThread {

View file

@ -12,6 +12,7 @@ import android.os.PowerManager
import android.provider.OpenableColumns import android.provider.OpenableColumns
import android.view.Window import android.view.Window
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import io.heckel.ntfy.R
import io.heckel.ntfy.db.Notification import io.heckel.ntfy.db.Notification
import io.heckel.ntfy.db.Repository import io.heckel.ntfy.db.Repository
import io.heckel.ntfy.db.Subscription import io.heckel.ntfy.db.Subscription
@ -129,7 +130,7 @@ fun formatTitle(notification: Notification): String {
// Checks in the most horrible way if a content URI exists; I couldn't find a better way // Checks in the most horrible way if a content URI exists; I couldn't find a better way
fun fileExists(context: Context, contentUri: String?): Boolean { fun fileExists(context: Context, contentUri: String?): Boolean {
return try { return try {
queryFilenameInternal(context, contentUri) // Throws if the file does not exist fileStat(context, Uri.parse(contentUri)) // Throws if the file does not exist
true true
} catch (_: Exception) { } catch (_: Exception) {
false false
@ -137,25 +138,35 @@ fun fileExists(context: Context, contentUri: String?): Boolean {
} }
// Queries the filename of a content URI // Queries the filename of a content URI
fun queryFilename(context: Context, contentUri: String?, fallbackName: String): String { fun fileName(context: Context, contentUri: String?, fallbackName: String): String {
return try { return try {
queryFilenameInternal(context, contentUri) val info = fileStat(context, Uri.parse(contentUri))
info.filename
} catch (_: Exception) { } catch (_: Exception) {
fallbackName fallbackName
} }
} }
fun queryFilenameInternal(context: Context, contentUri: String?): String { fun fileStat(context: Context, contentUri: Uri?): FileInfo {
if (contentUri == null) throw Exception("URI is null") if (contentUri == null) throw Exception("URI is null")
val resolver = context.applicationContext.contentResolver val resolver = context.applicationContext.contentResolver
val cursor = resolver.query(Uri.parse(contentUri), null, null, null, null) ?: throw Exception("Query returned null") val cursor = resolver.query(contentUri, null, null, null, null) ?: throw Exception("Query returned null")
return cursor.use { c -> return cursor.use { c ->
val nameIndex = c.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME) val nameIndex = c.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)
val sizeIndex = c.getColumnIndexOrThrow(OpenableColumns.SIZE)
c.moveToFirst() c.moveToFirst()
c.getString(nameIndex) FileInfo(
filename = c.getString(nameIndex),
size = c.getLong(sizeIndex)
)
} }
} }
data class FileInfo(
val filename: String,
val size: Long,
)
// Status bar color fading to match action bar, see https://stackoverflow.com/q/51150077/1440785 // Status bar color fading to match action bar, see https://stackoverflow.com/q/51150077/1440785
fun fadeStatusBarColor(window: Window, fromColor: Int, toColor: Int) { fun fadeStatusBarColor(window: Window, fromColor: Int, toColor: Int) {
val statusBarColorAnimation = ValueAnimator.ofObject(ArgbEvaluator(), fromColor, toColor) val statusBarColorAnimation = ValueAnimator.ofObject(ArgbEvaluator(), fromColor, toColor)
@ -195,6 +206,20 @@ fun formatBytes(bytes: Long, decimals: Int = 1): String {
return java.lang.String.format("%.${decimals}f %cB", value / 1024.0, ci.current()) return java.lang.String.format("%.${decimals}f %cB", value / 1024.0, ci.current())
} }
fun mimeTypeToIconResource(mimeType: String?): Int {
return if (mimeType?.startsWith("image/") == true) {
R.drawable.ic_file_image_red_24dp
} else if (mimeType?.startsWith("video/") == true) {
R.drawable.ic_file_video_orange_24dp
} else if (mimeType?.startsWith("audio/") == true) {
R.drawable.ic_file_audio_purple_24dp
} else if (mimeType == "application/vnd.android.package-archive") {
R.drawable.ic_file_app_gray_24dp
} else {
R.drawable.ic_file_document_blue_24dp
}
}
fun supportedImage(mimeType: String?): Boolean { fun supportedImage(mimeType: String?): Boolean {
return listOf("image/jpeg", "image/png").contains(mimeType) return listOf("image/jpeg", "image/png").contains(mimeType)
} }
@ -231,12 +256,10 @@ class ContentUriRequestBody(
private val contentResolver: ContentResolver, private val contentResolver: ContentResolver,
private val contentUri: Uri private val contentUri: Uri
) : RequestBody() { ) : RequestBody() {
override fun contentType(): MediaType? { override fun contentType(): MediaType? {
val contentType = contentResolver.getType(contentUri) val contentType = contentResolver.getType(contentUri)
return contentType?.toMediaTypeOrNull() return contentType?.toMediaTypeOrNull()
} }
override fun writeTo(sink: BufferedSink) { override fun writeTo(sink: BufferedSink) {
val inputStream = contentResolver.openInputStream(contentUri) ?: throw IOException("Couldn't open content URI for reading") val inputStream = contentResolver.openInputStream(contentUri) ?: throw IOException("Couldn't open content URI for reading")
inputStream.source().use { source -> inputStream.source().use { source ->

View file

@ -4,7 +4,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="horizontal" android:padding="10dp"> android:orientation="horizontal" android:paddingStart="15dp" android:paddingEnd="15dp" android:paddingTop="10dp" android:paddingBottom="10dp">
<ProgressBar <ProgressBar
style="?android:attr/progressBarStyle" style="?android:attr/progressBarStyle"
@ -21,7 +21,7 @@
android:paddingBottom="2dp" android:paddingBottom="2dp"
android:text="@string/share_content_title" android:text="@string/share_content_title"
android:textAlignment="viewStart" android:textAlignment="viewStart"
android:textAppearance="@style/TextAppearance.AppCompat.Medium" android:paddingStart="4dp" android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/> app:layout_constraintTop_toTopOf="parent"/>
<com.google.android.material.imageview.ShapeableImageView <com.google.android.material.imageview.ShapeableImageView
@ -31,7 +31,7 @@
android:scaleType="fitStart" android:scaleType="fitStart"
android:adjustViewBounds="true" android:maxHeight="150dp" android:adjustViewBounds="true" android:maxHeight="150dp"
app:shapeAppearanceOverlay="@style/roundedCornersImageView" android:visibility="visible" app:shapeAppearanceOverlay="@style/roundedCornersImageView" android:visibility="visible"
app:layout_constraintTop_toBottomOf="@id/share_content_title" android:layout_marginStart="3dp"/> app:layout_constraintTop_toBottomOf="@id/share_content_title" android:layout_marginTop="5dp"/>
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/share_content_text" android:id="@+id/share_content_text"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -39,6 +39,34 @@
android:importantForAutofill="no" android:importantForAutofill="no"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"
android:lines="10" android:gravity="start|top" app:layout_constraintTop_toBottomOf="@id/share_content_image" android:minLines="1" android:layout_marginTop="5dp"/> android:lines="10" android:gravity="start|top" app:layout_constraintTop_toBottomOf="@id/share_content_image" android:minLines="1" android:layout_marginTop="5dp"/>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" app:layout_constraintTop_toBottomOf="@id/share_content_text"
android:id="@+id/share_content_file_box" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:visibility="visible" android:layout_marginTop="5dp"
android:background="?android:attr/selectableItemBackground"
android:clickable="true" android:focusable="true" android:padding="4dp" android:paddingStart="0dp">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content" app:srcCompat="@drawable/ic_cancel_gray_24dp"
android:id="@+id/share_content_file_icon" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toStartOf="@+id/share_content_file_info" android:layout_marginEnd="5dp"
app:layout_constraintBottom_toBottomOf="parent"
/>
<TextView
android:text="some file.mp3\n7.1 MB"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:id="@+id/share_content_file_info"
android:textColor="?android:attr/textColorPrimary"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintStart_toEndOf="@+id/share_content_file_icon"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/share_content_file_icon"
app:layout_constraintBottom_toBottomOf="@+id/share_content_file_icon"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView <TextView
android:id="@+id/share_topic_title" android:id="@+id/share_topic_title"
android:layout_width="0dp" android:layout_width="0dp"
@ -47,9 +75,9 @@
android:paddingBottom="3dp" android:paddingBottom="3dp"
android:text="Share to topic" android:text="Share to topic"
android:textAlignment="viewStart" android:textAlignment="viewStart"
android:textAppearance="@style/TextAppearance.AppCompat.Medium" android:paddingStart="4dp" android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/share_content_text" android:layout_marginTop="10dp"/> app:layout_constraintTop_toBottomOf="@id/share_content_file_box" android:layout_marginTop="10dp"/>
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/share_topic_text" android:id="@+id/share_topic_text"
android:layout_width="match_parent" android:layout_width="match_parent"

View file

@ -87,13 +87,13 @@
app:layout_constraintStart_toStartOf="parent" android:layout_marginStart="10dp" app:layout_constraintStart_toStartOf="parent" android:layout_marginStart="10dp"
app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="10dp" app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="10dp"
app:layout_constraintTop_toBottomOf="@id/detail_item_attachment_image" app:layout_constraintTop_toBottomOf="@id/detail_item_attachment_image"
app:layout_constraintBottom_toTopOf="@id/detail_item_attachment_box" app:layout_constraintBottom_toTopOf="@id/share_content_file_box"
app:layout_constraintHorizontal_bias="0.0" android:layout_marginTop="2dp" app:layout_constraintHorizontal_bias="0.0" android:layout_marginTop="2dp"
/> />
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" app:layout_constraintTop_toBottomOf="@id/detail_item_tags_text" android:layout_height="wrap_content" app:layout_constraintTop_toBottomOf="@id/detail_item_tags_text"
android:id="@+id/detail_item_attachment_box" app:layout_constraintStart_toStartOf="parent" android:id="@+id/share_content_file_box" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" android:layout_marginStart="10dp" android:layout_marginEnd="10dp" app:layout_constraintEnd_toEndOf="parent" android:layout_marginStart="10dp" android:layout_marginEnd="10dp"
app:layout_constraintBottom_toTopOf="@id/detail_item_padding_bottom" app:layout_constraintBottom_toTopOf="@id/detail_item_padding_bottom"
android:visibility="visible" android:layout_marginTop="2dp" android:visibility="visible" android:layout_marginTop="2dp"
@ -102,27 +102,27 @@
<ImageView <ImageView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" app:srcCompat="@drawable/ic_cancel_gray_24dp" android:layout_height="wrap_content" app:srcCompat="@drawable/ic_cancel_gray_24dp"
android:id="@+id/detail_item_attachment_icon" app:layout_constraintStart_toStartOf="parent" android:id="@+id/share_content_file_icon" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toStartOf="@+id/detail_item_attachment_info" android:layout_marginEnd="5dp" app:layout_constraintEnd_toStartOf="@+id/share_content_file_info" android:layout_marginEnd="5dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
/> />
<TextView <TextView
android:text="attachment.jpg\n58 MB, not downloaded, expires 1/2/2022 10:30 PM" android:text="attachment.jpg\n58 MB, not downloaded, expires 1/2/2022 10:30 PM"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:id="@+id/detail_item_attachment_info" android:id="@+id/share_content_file_info"
android:textColor="?android:attr/textColorPrimary" android:textColor="?android:attr/textColorPrimary"
android:textAppearance="@style/TextAppearance.AppCompat.Small" android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintStart_toEndOf="@+id/detail_item_attachment_icon" app:layout_constraintStart_toEndOf="@+id/share_content_file_icon"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/detail_item_attachment_icon" app:layout_constraintTop_toTopOf="@+id/share_content_file_icon"
app:layout_constraintBottom_toBottomOf="@+id/detail_item_attachment_icon"/> app:layout_constraintBottom_toBottomOf="@+id/share_content_file_icon"/>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<TextView <TextView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="5dp" android:id="@+id/detail_item_padding_bottom" android:layout_height="5dp" android:id="@+id/detail_item_padding_bottom"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/detail_item_attachment_box"/> app:layout_constraintTop_toBottomOf="@id/share_content_file_box"/>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -191,8 +191,10 @@
<string name="share_content_title">Message preview</string> <string name="share_content_title">Message preview</string>
<string name="share_content_text_hint">Add the content you\'d like to share here</string> <string name="share_content_text_hint">Add the content you\'d like to share here</string>
<string name="share_content_image_text">An image was shared with you</string> <string name="share_content_image_text">An image was shared with you</string>
<string name="share_content_image_error">Cannot read image</string> <string name="share_content_image_error">Cannot read image: %1$s</string>
<string name="share_content_file_text">An file was shared with you</string> <string name="share_content_file_text">A file was shared with you</string>
<string name="share_content_file_error">Cannot read file infos: %1$s</string>
<string name="share_successful">Message successfully published</string>
<!-- Notification dialog --> <!-- Notification dialog -->
<string name="notification_dialog_title">Pause notifications</string> <string name="notification_dialog_title">Pause notifications</string>