diff --git a/app/src/main/java/io/heckel/ntfy/msg/ApiService.kt b/app/src/main/java/io/heckel/ntfy/msg/ApiService.kt index 89ce1b8..df48066 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/ApiService.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/ApiService.kt @@ -29,7 +29,7 @@ class ApiService { .build() private val parser = NotificationParser() - fun publish(baseUrl: String, topic: String, user: User?, message: String, title: String, priority: Int, tags: List, delay: String, body: RequestBody? = null) { + fun publish(baseUrl: String, topic: String, user: User?, message: String, title: String, priority: Int, tags: List, delay: String, body: RequestBody? = null, filename: String = "") { val url = topicUrl(baseUrl, topic) Log.d(TAG, "Publishing to $url") @@ -46,6 +46,9 @@ class ApiService { if (delay.isNotEmpty()) { builder.addHeader("X-Delay", delay) } + if (filename.isNotEmpty()) { + builder.addHeader("X-Filename", filename) + } if (body != null) { builder .addHeader("X-Message", message) diff --git a/app/src/main/java/io/heckel/ntfy/msg/DownloadWorker.kt b/app/src/main/java/io/heckel/ntfy/msg/DownloadWorker.kt index f094a73..d66af06 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/DownloadWorker.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/DownloadWorker.kt @@ -18,7 +18,7 @@ import io.heckel.ntfy.R import io.heckel.ntfy.app.Application import io.heckel.ntfy.db.* import io.heckel.ntfy.util.Log -import io.heckel.ntfy.util.queryFilename +import io.heckel.ntfy.util.fileName import okhttp3.OkHttpClient import okhttp3.Request 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") - val actualName = queryFilename(context, uri.toString(), attachment.name) + val actualName = fileName(context, uri.toString(), attachment.name) save(attachment.copy( name = actualName, size = bytesCopied, diff --git a/app/src/main/java/io/heckel/ntfy/ui/AddFragment.kt b/app/src/main/java/io/heckel/ntfy/ui/AddFragment.kt index f32c0d2..4f60404 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/AddFragment.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/AddFragment.kt @@ -110,7 +110,6 @@ class AddFragment : DialogFragment() { endIconImageView.minimumWidth = dimension.toInt() subscribeBaseUrlLayout.requestLayout() - // Fields for "login page" loginUsernameText = view.findViewById(R.id.add_dialog_login_username) loginPasswordText = view.findViewById(R.id.add_dialog_login_password) diff --git a/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt b/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt index 6029d16..7968381 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt @@ -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 menuButton: ImageButton = itemView.findViewById(R.id.detail_item_menu_button) 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 attachmentIconView: ImageView = itemView.findViewById(R.id.detail_item_attachment_icon) - private val attachmentInfoView: TextView = itemView.findViewById(R.id.detail_item_attachment_info) + private val attachmentBoxView: View = itemView.findViewById(R.id.share_content_file_box) + private val attachmentIconView: ImageView = itemView.findViewById(R.id.share_content_file_icon) + private val attachmentInfoView: TextView = itemView.findViewById(R.id.share_content_file_info) fun bind(notification: Notification) { this.notification = notification @@ -157,17 +157,7 @@ class DetailAdapter(private val activity: Activity, private val repository: Repo return } attachmentInfoView.text = formatAttachmentDetails(context, attachment, exists) - attachmentIconView.setImageResource(if (attachment.type?.startsWith("image/") == true) { - 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 - }) + attachmentIconView.setImageResource(mimeTypeToIconResource(attachment.type)) val attachmentBoxPopupMenu = createAttachmentPopup(context, attachmentBoxView, notification, attachment, exists) // Heavy lifting not during on-click if (attachmentBoxPopupMenu != null) { 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 { - val name = queryFilename(context, attachment.contentUri, attachment.name) + val name = fileName(context, attachment.contentUri, attachment.name) val notYetDownloaded = !exists && attachment.progress == PROGRESS_NONE val downloading = !exists && attachment.progress in 0..99 val deleted = !exists && (attachment.progress == PROGRESS_DONE || attachment.progress == PROGRESS_DELETED) diff --git a/app/src/main/java/io/heckel/ntfy/ui/ShareActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/ShareActivity.kt index b66a5e0..7d43079 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/ShareActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/ShareActivity.kt @@ -13,14 +13,13 @@ import android.view.View import android.widget.ImageView import android.widget.ProgressBar import android.widget.TextView +import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope import io.heckel.ntfy.R import io.heckel.ntfy.app.Application import io.heckel.ntfy.msg.ApiService -import io.heckel.ntfy.util.ContentUriRequestBody -import io.heckel.ntfy.util.Log -import io.heckel.ntfy.util.supportedImage +import io.heckel.ntfy.util.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -35,6 +34,9 @@ class ShareActivity : AppCompatActivity() { private lateinit var menu: Menu private lateinit var sendItem: MenuItem 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 topicText: TextView private lateinit var progress: ProgressBar @@ -57,6 +59,9 @@ class ShareActivity : AppCompatActivity() { // UI elements contentText = findViewById(R.id.share_content_text) 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) progress = findViewById(R.id.share_progress) progress.visibility = View.GONE @@ -93,8 +98,8 @@ class ShareActivity : AppCompatActivity() { private fun handleSendText(intent: Intent) { intent.getStringExtra(Intent.EXTRA_TEXT)?.let { text -> - contentImage.visibility = View.GONE contentText.text = text + show() } } @@ -105,18 +110,33 @@ class ShareActivity : AppCompatActivity() { val bitmapStream = resolver.openInputStream(fileUri!!) val bitmap = BitmapFactory.decodeStream(bitmapStream) contentImage.setImageBitmap(bitmap) - contentImage.visibility = View.VISIBLE contentText.text = getString(R.string.share_content_image_text) - } catch (_: Exception) { + show(image = true) + } catch (e: Exception) { fileUri = null - contentImage.visibility = View.GONE - contentText.text = getString(R.string.share_content_image_error) + contentText.text = "" + errorText.text = getString(R.string.share_content_image_error, e.message) + show(error = true) } } private fun handleSendFile(intent: Intent) { fileUri = intent.getParcelableExtra(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 { @@ -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() { val baseUrl = "https://ntfy.sh" // FIXME val topic = topicText.text.toString() @@ -150,6 +177,11 @@ class ShareActivity : AppCompatActivity() { lifecycleScope.launch(Dispatchers.IO) { val user = repository.getUser(baseUrl) try { + val filename = if (fileUri != null) { + fileStat(this@ShareActivity, fileUri).filename + } else { + "" + } val body = if (fileUri != null) { val resolver = applicationContext.contentResolver ContentUriRequestBody(resolver, fileUri!!) @@ -165,10 +197,14 @@ class ShareActivity : AppCompatActivity() { priority = 3, tags = emptyList(), delay = "", - body = body // May be null + body = body, // May be null + filename = filename // May be empty ) runOnUiThread { finish() + Toast + .makeText(this@ShareActivity, getString(R.string.share_successful), Toast.LENGTH_LONG) + .show() } } catch (e: Exception) { runOnUiThread { diff --git a/app/src/main/java/io/heckel/ntfy/util/Util.kt b/app/src/main/java/io/heckel/ntfy/util/Util.kt index 5b87904..37907b2 100644 --- a/app/src/main/java/io/heckel/ntfy/util/Util.kt +++ b/app/src/main/java/io/heckel/ntfy/util/Util.kt @@ -12,6 +12,7 @@ import android.os.PowerManager import android.provider.OpenableColumns import android.view.Window import androidx.appcompat.app.AppCompatDelegate +import io.heckel.ntfy.R import io.heckel.ntfy.db.Notification import io.heckel.ntfy.db.Repository 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 fun fileExists(context: Context, contentUri: String?): Boolean { 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 } catch (_: Exception) { false @@ -137,25 +138,35 @@ fun fileExists(context: Context, contentUri: String?): Boolean { } // 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 { - queryFilenameInternal(context, contentUri) + val info = fileStat(context, Uri.parse(contentUri)) + info.filename } catch (_: Exception) { fallbackName } } -fun queryFilenameInternal(context: Context, contentUri: String?): String { +fun fileStat(context: Context, contentUri: Uri?): FileInfo { if (contentUri == null) throw Exception("URI is null") 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 -> val nameIndex = c.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME) + val sizeIndex = c.getColumnIndexOrThrow(OpenableColumns.SIZE) 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 fun fadeStatusBarColor(window: Window, fromColor: Int, toColor: Int) { 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()) } +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 { return listOf("image/jpeg", "image/png").contains(mimeType) } @@ -231,12 +256,10 @@ class ContentUriRequestBody( private val contentResolver: ContentResolver, private val contentUri: Uri ) : RequestBody() { - override fun contentType(): MediaType? { val contentType = contentResolver.getType(contentUri) return contentType?.toMediaTypeOrNull() } - override fun writeTo(sink: BufferedSink) { val inputStream = contentResolver.openInputStream(contentUri) ?: throw IOException("Couldn't open content URI for reading") inputStream.source().use { source -> diff --git a/app/src/main/res/layout/activity_share.xml b/app/src/main/res/layout/activity_share.xml index c50190f..d7e7697 100644 --- a/app/src/main/res/layout/activity_share.xml +++ b/app/src/main/res/layout/activity_share.xml @@ -4,7 +4,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="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"> + app:layout_constraintTop_toBottomOf="@id/share_content_title" android:layout_marginTop="5dp"/> + + + + + app:layout_constraintTop_toBottomOf="@id/share_content_file_box" android:layout_marginTop="10dp"/> + app:layout_constraintTop_toTopOf="@+id/share_content_file_icon" + app:layout_constraintBottom_toBottomOf="@+id/share_content_file_icon"/> + app:layout_constraintTop_toBottomOf="@id/share_content_file_box"/> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 38ebed8..b112dee 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -191,8 +191,10 @@ Message preview Add the content you\'d like to share here An image was shared with you - Cannot read image - An file was shared with you + Cannot read image: %1$s + A file was shared with you + Cannot read file infos: %1$s + Message successfully published Pause notifications