From ac0ecbdcc19ffe19ab1ad28270dbc0d733f79166 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Sun, 20 Mar 2022 19:53:51 -0400 Subject: [PATCH] Open "Click" link when tapping notification, #110 --- .../java/io/heckel/ntfy/ui/DetailActivity.kt | 21 ++- .../java/io/heckel/ntfy/ui/DetailAdapter.kt | 176 ++++++++++-------- app/src/main/java/io/heckel/ntfy/util/Util.kt | 14 ++ .../main/res/menu/menu_detail_attachment.xml | 1 + app/src/main/res/values/strings.xml | 3 + .../metadata/android/en-US/changelog/24.txt | 1 + 6 files changed, 126 insertions(+), 90 deletions(-) diff --git a/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt index 6008765..cad7da5 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt @@ -5,6 +5,7 @@ import android.content.ClipData import android.content.ClipboardManager import android.content.Context import android.content.Intent +import android.net.Uri import android.os.Bundle import android.text.Html import android.util.Base64 @@ -509,6 +510,17 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra private fun onNotificationClick(notification: Notification) { if (actionMode != null) { handleActionModeClick(notification) + } else if (notification.click != "") { + try { + startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(notification.click))) + } catch (e: Exception) { + Log.w(TAG, "Cannot open click URL", e) + runOnUiThread { + Toast + .makeText(this@DetailActivity, getString(R.string.detail_item_cannot_open_click_url, e.message), Toast.LENGTH_LONG) + .show() + } + } } else { copyToClipboard(notification) } @@ -516,14 +528,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra private fun copyToClipboard(notification: Notification) { runOnUiThread { - val message = decodeMessage(notification) - val text = message + "\n\n" + Date(notification.timestamp * 1000).toString() - val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - val clip = ClipData.newPlainText("notification message", text) - clipboard.setPrimaryClip(clip) - Toast - .makeText(this, getString(R.string.detail_copied_to_clipboard_message), Toast.LENGTH_LONG) - .show() + copyToClipboard(this, notification) } } 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 2996e6d..f910e9b 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt @@ -20,13 +20,11 @@ import androidx.recyclerview.widget.RecyclerView import com.stfalcon.imageviewer.StfalconImageViewer import io.heckel.ntfy.R import io.heckel.ntfy.db.* -import io.heckel.ntfy.util.Log import io.heckel.ntfy.msg.DownloadManager import io.heckel.ntfy.util.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch -import java.util.* class DetailAdapter(private val activity: Activity, private val repository: Repository, private val onClick: (Notification) -> Unit, private val onLongClick: (Notification) -> Unit) : ListAdapter(TopicDiffCallback) { @@ -98,8 +96,11 @@ class DetailAdapter(private val activity: Activity, private val repository: Repo if (selected.contains(notification.id)) { itemView.setBackgroundResource(Colors.itemSelectedBackground(context)) } + val attachment = notification.attachment + val exists = if (attachment?.contentUri != null) fileExists(context, attachment.contentUri) else false renderPriority(context, notification) - maybeRenderAttachment(context, notification) + maybeRenderMenu(context, notification, exists) + maybeRenderAttachment(context, notification, exists) } private fun renderPriority(context: Context, notification: Notification) { @@ -126,23 +127,20 @@ class DetailAdapter(private val activity: Activity, private val repository: Repo } } - private fun maybeRenderAttachment(context: Context, notification: Notification) { + private fun maybeRenderAttachment(context: Context, notification: Notification, exists: Boolean) { if (notification.attachment == null) { - menuButton.visibility = View.GONE attachmentImageView.visibility = View.GONE attachmentBoxView.visibility = View.GONE return } val attachment = notification.attachment - val exists = if (attachment.contentUri != null) fileExists(context, attachment.contentUri) else false val image = attachment.contentUri != null && exists && supportedImage(attachment.type) - maybeRenderMenu(context, notification, attachment, exists) maybeRenderAttachmentImage(context, attachment, image) maybeRenderAttachmentBox(context, notification, attachment, exists, image) } - private fun maybeRenderMenu(context: Context, notification: Notification, attachment: Attachment, exists: Boolean) { - val menuButtonPopupMenu = createAttachmentPopup(context, menuButton, notification, attachment, exists) // Heavy lifting not during on-click + private fun maybeRenderMenu(context: Context, notification: Notification, exists: Boolean) { + val menuButtonPopupMenu = maybeCreateMenuPopup(context, menuButton, notification, exists) // Heavy lifting not during on-click if (menuButtonPopupMenu != null) { menuButton.setOnClickListener { menuButtonPopupMenu.show() } menuButton.visibility = View.VISIBLE @@ -158,7 +156,7 @@ class DetailAdapter(private val activity: Activity, private val repository: Repo } attachmentInfoView.text = formatAttachmentDetails(context, attachment, exists) attachmentIconView.setImageResource(mimeTypeToIconResource(attachment.type)) - val attachmentBoxPopupMenu = createAttachmentPopup(context, attachmentBoxView, notification, attachment, exists) // Heavy lifting not during on-click + val attachmentBoxPopupMenu = maybeCreateMenuPopup(context, attachmentBoxView, notification, exists) // Heavy lifting not during on-click if (attachmentBoxPopupMenu != null) { attachmentBoxView.setOnClickListener { attachmentBoxPopupMenu.show() } } else { @@ -171,93 +169,107 @@ class DetailAdapter(private val activity: Activity, private val repository: Repo attachmentBoxView.visibility = View.VISIBLE } - private fun createAttachmentPopup(context: Context, anchor: View?, notification: Notification, attachment: Attachment, exists: Boolean): PopupMenu? { + private fun maybeCreateMenuPopup(context: Context, anchor: View?, notification: Notification, exists: Boolean): PopupMenu? { val popup = PopupMenu(context, anchor) popup.menuInflater.inflate(R.menu.menu_detail_attachment, popup.menu) + val attachment = notification.attachment // May be null + val hasAttachment = attachment != null val downloadItem = popup.menu.findItem(R.id.detail_item_menu_download) val cancelItem = popup.menu.findItem(R.id.detail_item_menu_cancel) val openItem = popup.menu.findItem(R.id.detail_item_menu_open) val browseItem = popup.menu.findItem(R.id.detail_item_menu_browse) val deleteItem = popup.menu.findItem(R.id.detail_item_menu_delete) val copyUrlItem = popup.menu.findItem(R.id.detail_item_menu_copy_url) - val expired = attachment.expires != null && attachment.expires < System.currentTimeMillis()/1000 - val inProgress = attachment.progress in 0..99 - if (attachment.contentUri != null) { - openItem.setOnMenuItemClickListener { - try { - val contentUri = Uri.parse(attachment.contentUri) - val intent = Intent(Intent.ACTION_VIEW, contentUri) - intent.setDataAndType(contentUri, attachment.type ?: "application/octet-stream") // Required for Android <= P - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - context.startActivity(intent) - } catch (e: ActivityNotFoundException) { - Toast - .makeText(context, context.getString(R.string.detail_item_cannot_open_not_found), Toast.LENGTH_LONG) - .show() - } catch (e: Exception) { - Toast - .makeText(context, context.getString(R.string.detail_item_cannot_open, e.message), Toast.LENGTH_LONG) - .show() - } - true - } - } - browseItem.setOnMenuItemClickListener { - val intent = Intent(android.app.DownloadManager.ACTION_VIEW_DOWNLOADS) - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - context.startActivity(intent) - true - } - if (attachment.contentUri != null) { - deleteItem.setOnMenuItemClickListener { - try { - val contentUri = Uri.parse(attachment.contentUri) - val resolver = context.applicationContext.contentResolver - val deleted = resolver.delete(contentUri, null, null) > 0 - if (!deleted) throw Exception("no rows deleted") - val newAttachment = attachment.copy(progress = PROGRESS_DELETED) - val newNotification = notification.copy(attachment = newAttachment) - GlobalScope.launch(Dispatchers.IO) { - repository.updateNotification(newNotification) + val copyContentsItem = popup.menu.findItem(R.id.detail_item_menu_contents) + val expired = attachment?.expires != null && attachment.expires < System.currentTimeMillis()/1000 + val inProgress = attachment?.progress in 0..99 + if (attachment != null) { + if (attachment.contentUri != null) { + openItem.setOnMenuItemClickListener { + try { + val contentUri = Uri.parse(attachment.contentUri) + val intent = Intent(Intent.ACTION_VIEW, contentUri) + intent.setDataAndType(contentUri, attachment.type ?: "application/octet-stream") // Required for Android <= P + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + context.startActivity(intent) + } catch (e: ActivityNotFoundException) { + Toast + .makeText(context, context.getString(R.string.detail_item_cannot_open_not_found), Toast.LENGTH_LONG) + .show() + } catch (e: Exception) { + Toast + .makeText(context, context.getString(R.string.detail_item_cannot_open, e.message), Toast.LENGTH_LONG) + .show() } - } catch (e: Exception) { - Log.w(TAG, "Failed to update notification: ${e.message}", e) - Toast - .makeText(context, context.getString(R.string.detail_item_delete_failed, e.message), Toast.LENGTH_LONG) - .show() + true } + } + browseItem.setOnMenuItemClickListener { + val intent = Intent(android.app.DownloadManager.ACTION_VIEW_DOWNLOADS) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + context.startActivity(intent) + true + } + if (attachment.contentUri != null) { + deleteItem.setOnMenuItemClickListener { + try { + val contentUri = Uri.parse(attachment.contentUri) + val resolver = context.applicationContext.contentResolver + val deleted = resolver.delete(contentUri, null, null) > 0 + if (!deleted) throw Exception("no rows deleted") + val newAttachment = attachment.copy(progress = PROGRESS_DELETED) + val newNotification = notification.copy(attachment = newAttachment) + GlobalScope.launch(Dispatchers.IO) { + repository.updateNotification(newNotification) + } + } catch (e: Exception) { + Log.w(TAG, "Failed to update notification: ${e.message}", e) + Toast + .makeText(context, context.getString(R.string.detail_item_delete_failed, e.message), Toast.LENGTH_LONG) + .show() + } + true + } + } + copyUrlItem.setOnMenuItemClickListener { + val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = ClipData.newPlainText("attachment url", attachment.url) + clipboard.setPrimaryClip(clip) + Toast + .makeText(context, context.getString(R.string.detail_item_menu_copy_url_copied), Toast.LENGTH_LONG) + .show() + true + } + downloadItem.setOnMenuItemClickListener { + val requiresPermission = Build.VERSION.SDK_INT <= Build.VERSION_CODES.P && ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED + if (requiresPermission) { + ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), REQUEST_CODE_WRITE_STORAGE_PERMISSION_FOR_DOWNLOAD) + return@setOnMenuItemClickListener true + } + DownloadManager.enqueue(context, notification.id, userAction = true) + true + } + cancelItem.setOnMenuItemClickListener { + DownloadManager.cancel(context, notification.id) true } } - copyUrlItem.setOnMenuItemClickListener { - val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - val clip = ClipData.newPlainText("attachment url", attachment.url) - clipboard.setPrimaryClip(clip) - Toast - .makeText(context, context.getString(R.string.detail_item_menu_copy_url_copied), Toast.LENGTH_LONG) - .show() - true - } - downloadItem.setOnMenuItemClickListener { - val requiresPermission = Build.VERSION.SDK_INT <= Build.VERSION_CODES.P && ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED - if (requiresPermission) { - ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), REQUEST_CODE_WRITE_STORAGE_PERMISSION_FOR_DOWNLOAD) - return@setOnMenuItemClickListener true + if (notification.click != "") { + copyContentsItem.setOnMenuItemClickListener { + copyToClipboard(context, notification) + true } - DownloadManager.enqueue(context, notification.id, userAction = true) - true } - cancelItem.setOnMenuItemClickListener { - DownloadManager.cancel(context, notification.id) - true - } - openItem.isVisible = exists - browseItem.isVisible = exists - downloadItem.isVisible = !exists && !expired && !inProgress - deleteItem.isVisible = exists - copyUrlItem.isVisible = !expired - cancelItem.isVisible = inProgress - val noOptions = !openItem.isVisible && !browseItem.isVisible && !downloadItem.isVisible && !copyUrlItem.isVisible && !cancelItem.isVisible && !deleteItem.isVisible + openItem.isVisible = hasAttachment && exists + browseItem.isVisible = hasAttachment && exists + downloadItem.isVisible = hasAttachment && !exists && !expired && !inProgress + deleteItem.isVisible = hasAttachment && exists + copyUrlItem.isVisible = hasAttachment && !expired + cancelItem.isVisible = hasAttachment && inProgress + copyContentsItem.isVisible = notification.click != "" + val noOptions = !openItem.isVisible && !browseItem.isVisible && !downloadItem.isVisible + && !copyUrlItem.isVisible && !cancelItem.isVisible && !deleteItem.isVisible + && !copyContentsItem.isVisible if (noOptions) { return null } 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 18e28c7..fa2b40c 100644 --- a/app/src/main/java/io/heckel/ntfy/util/Util.kt +++ b/app/src/main/java/io/heckel/ntfy/util/Util.kt @@ -2,6 +2,8 @@ package io.heckel.ntfy.util import android.animation.ArgbEvaluator import android.animation.ValueAnimator +import android.content.ClipData +import android.content.ClipboardManager import android.content.ContentResolver import android.content.Context import android.content.res.Configuration @@ -18,6 +20,7 @@ import android.util.TypedValue import android.view.View import android.view.Window import android.widget.ImageView +import android.widget.Toast import androidx.appcompat.app.AppCompatDelegate import io.heckel.ntfy.R import io.heckel.ntfy.db.Notification @@ -366,3 +369,14 @@ fun ensureSafeNewFile(dir: File, name: String): File { } throw Exception("Cannot find safe file") } + +fun copyToClipboard(context: Context, notification: Notification) { + val message = decodeMessage(notification) + val text = message + "\n\n" + formatDateShort(notification.timestamp) + val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = ClipData.newPlainText("notification message", text) + clipboard.setPrimaryClip(clip) + Toast + .makeText(context, context.getString(R.string.detail_copied_to_clipboard_message), Toast.LENGTH_LONG) + .show() +} diff --git a/app/src/main/res/menu/menu_detail_attachment.xml b/app/src/main/res/menu/menu_detail_attachment.xml index 8d623bb..1601ad5 100644 --- a/app/src/main/res/menu/menu_detail_attachment.xml +++ b/app/src/main/res/menu/menu_detail_attachment.xml @@ -6,4 +6,5 @@ + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9be76ae..6f99532 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -144,9 +144,12 @@ Cancel download Copy URL Copied URL to clipboard + Copy notification + Copied notification clipboard Cannot open or download attachment. Link expired and no local file found. Cannot open attachment: %1$s Cannot open attachment: File may have been deleted, or there is no app to open the file. + Cannot open click URL: %1$s Cannot delete attachment: %1$s Attachment download failed: %1$s not downloaded diff --git a/fastlane/metadata/android/en-US/changelog/24.txt b/fastlane/metadata/android/en-US/changelog/24.txt index 93ad733..7e1ea84 100644 --- a/fastlane/metadata/android/en-US/changelog/24.txt +++ b/fastlane/metadata/android/en-US/changelog/24.txt @@ -1,6 +1,7 @@ Features: * Support for UnifiedPush 2.0 specification (bytes messages, #130) * Export/import settings and subscriptions (#115, thanks @cmeis for reporting) +* Open "Click" link when tapping notification (#110, thanks @cmeis for reporting) * JSON stream deprecation banner (#164) Bugs: