Open "Click" link when tapping notification, #110

This commit is contained in:
Philipp Heckel 2022-03-20 19:53:51 -04:00
parent adbc247279
commit ac0ecbdcc1
6 changed files with 126 additions and 90 deletions

View file

@ -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)
}
}

View file

@ -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<Notification, DetailAdapter.DetailViewHolder>(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
}

View file

@ -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()
}

View file

@ -6,4 +6,5 @@
<item android:id="@+id/detail_item_menu_browse" android:title="@string/detail_item_menu_browse"/>
<item android:id="@+id/detail_item_menu_delete" android:title="@string/detail_item_menu_delete"/>
<item android:id="@+id/detail_item_menu_copy_url" android:title="@string/detail_item_menu_copy_url"/>
<item android:id="@+id/detail_item_menu_contents" android:title="@string/detail_item_menu_copy_contents"/>
</menu>

View file

@ -144,9 +144,12 @@
<string name="detail_item_menu_cancel">Cancel download</string>
<string name="detail_item_menu_copy_url">Copy URL</string>
<string name="detail_item_menu_copy_url_copied">Copied URL to clipboard</string>
<string name="detail_item_menu_copy_contents">Copy notification</string>
<string name="detail_item_menu_copy_contents_copied">Copied notification clipboard</string>
<string name="detail_item_cannot_download">Cannot open or download attachment. Link expired and no local file found.</string>
<string name="detail_item_cannot_open">Cannot open attachment: %1$s</string>
<string name="detail_item_cannot_open_not_found">Cannot open attachment: File may have been deleted, or there is no app to open the file.</string>
<string name="detail_item_cannot_open_click_url">Cannot open click URL: %1$s</string>
<string name="detail_item_delete_failed">Cannot delete attachment: %1$s</string>
<string name="detail_item_download_failed">Attachment download failed: %1$s</string>
<string name="detail_item_download_info_not_downloaded">not downloaded</string>

View file

@ -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: