Make image and text sharing work
This commit is contained in:
parent
3b30e39eb5
commit
8100e68b8d
7 changed files with 130 additions and 22 deletions
|
@ -64,11 +64,10 @@
|
||||||
<action android:name="android.intent.action.SEND" />
|
<action android:name="android.intent.action.SEND" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<data android:mimeType="image/*" />
|
<data android:mimeType="image/*" />
|
||||||
</intent-filter>
|
<data android:mimeType="text/*" />
|
||||||
<intent-filter>
|
<data android:mimeType="audio/*" />
|
||||||
<action android:name="android.intent.action.SEND" />
|
<data android:mimeType="video/*" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<data android:mimeType="application/*" />
|
||||||
<data android:mimeType="text/plain" />
|
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package io.heckel.ntfy.msg
|
package io.heckel.ntfy.msg
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import io.heckel.ntfy.BuildConfig
|
import io.heckel.ntfy.BuildConfig
|
||||||
import io.heckel.ntfy.db.Notification
|
import io.heckel.ntfy.db.Notification
|
||||||
|
@ -28,12 +29,11 @@ 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) {
|
fun publish(baseUrl: String, topic: String, user: User?, message: String, title: String, priority: Int, tags: List<String>, delay: String, body: RequestBody? = null) {
|
||||||
val url = topicUrl(baseUrl, topic)
|
val url = topicUrl(baseUrl, topic)
|
||||||
Log.d(TAG, "Publishing to $url")
|
Log.d(TAG, "Publishing to $url")
|
||||||
|
|
||||||
val builder = requestBuilder(url, user)
|
val builder = requestBuilder(url, user)
|
||||||
.put(message.toRequestBody())
|
|
||||||
if (priority in 1..5) {
|
if (priority in 1..5) {
|
||||||
builder.addHeader("X-Priority", priority.toString())
|
builder.addHeader("X-Priority", priority.toString())
|
||||||
}
|
}
|
||||||
|
@ -46,6 +46,13 @@ class ApiService {
|
||||||
if (delay.isNotEmpty()) {
|
if (delay.isNotEmpty()) {
|
||||||
builder.addHeader("X-Delay", delay)
|
builder.addHeader("X-Delay", delay)
|
||||||
}
|
}
|
||||||
|
if (body != null) {
|
||||||
|
builder
|
||||||
|
.addHeader("X-Message", message)
|
||||||
|
.put(body)
|
||||||
|
} else {
|
||||||
|
builder.put(message.toRequestBody())
|
||||||
|
}
|
||||||
client.newCall(builder.build()).execute().use { response ->
|
client.newCall(builder.build()).execute().use { response ->
|
||||||
if (response.code == 401 || response.code == 403) {
|
if (response.code == 401 || response.code == 403) {
|
||||||
throw UnauthorizedException(user)
|
throw UnauthorizedException(user)
|
||||||
|
|
|
@ -193,7 +193,7 @@ class AddFragment : DialogFragment() {
|
||||||
subscribeInstantDeliveryBox.visibility = if (BuildConfig.FIREBASE_AVAILABLE) View.VISIBLE else View.GONE
|
subscribeInstantDeliveryBox.visibility = if (BuildConfig.FIREBASE_AVAILABLE) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
// Username/password validation on type
|
// Username/password validation on type
|
||||||
val textWatcher = object : TextWatcher {
|
val loginTextWatcher = object : TextWatcher {
|
||||||
override fun afterTextChanged(s: Editable?) {
|
override fun afterTextChanged(s: Editable?) {
|
||||||
validateInputLoginView()
|
validateInputLoginView()
|
||||||
}
|
}
|
||||||
|
@ -204,8 +204,8 @@ class AddFragment : DialogFragment() {
|
||||||
// Nothing
|
// Nothing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
loginUsernameText.addTextChangedListener(textWatcher)
|
loginUsernameText.addTextChangedListener(loginTextWatcher)
|
||||||
loginPasswordText.addTextChangedListener(textWatcher)
|
loginPasswordText.addTextChangedListener(loginTextWatcher)
|
||||||
|
|
||||||
// Build dialog
|
// Build dialog
|
||||||
val dialog = AlertDialog.Builder(activity)
|
val dialog = AlertDialog.Builder(activity)
|
||||||
|
@ -219,7 +219,7 @@ class AddFragment : DialogFragment() {
|
||||||
.create()
|
.create()
|
||||||
|
|
||||||
// Show keyboard when the dialog is shown (see https://stackoverflow.com/a/19573049/1440785)
|
// Show keyboard when the dialog is shown (see https://stackoverflow.com/a/19573049/1440785)
|
||||||
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
|
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)
|
||||||
|
|
||||||
// Add logic to disable "Subscribe" button on invalid input
|
// Add logic to disable "Subscribe" button on invalid input
|
||||||
dialog.setOnShowListener {
|
dialog.setOnShowListener {
|
||||||
|
@ -232,7 +232,7 @@ class AddFragment : DialogFragment() {
|
||||||
negativeButton.setOnClickListener {
|
negativeButton.setOnClickListener {
|
||||||
negativeButtonClick()
|
negativeButtonClick()
|
||||||
}
|
}
|
||||||
val textWatcher = object : TextWatcher {
|
val subscribeTextWatcher = object : TextWatcher {
|
||||||
override fun afterTextChanged(s: Editable?) {
|
override fun afterTextChanged(s: Editable?) {
|
||||||
validateInputSubscribeView()
|
validateInputSubscribeView()
|
||||||
}
|
}
|
||||||
|
@ -243,8 +243,8 @@ class AddFragment : DialogFragment() {
|
||||||
// Nothing
|
// Nothing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
subscribeTopicText.addTextChangedListener(textWatcher)
|
subscribeTopicText.addTextChangedListener(subscribeTextWatcher)
|
||||||
subscribeBaseUrlText.addTextChangedListener(textWatcher)
|
subscribeBaseUrlText.addTextChangedListener(subscribeTextWatcher)
|
||||||
subscribeInstantDeliveryCheckbox.setOnCheckedChangeListener { _, isChecked ->
|
subscribeInstantDeliveryCheckbox.setOnCheckedChangeListener { _, isChecked ->
|
||||||
if (isChecked) subscribeInstantDeliveryDescription.visibility = View.VISIBLE
|
if (isChecked) subscribeInstantDeliveryDescription.visibility = View.VISIBLE
|
||||||
else subscribeInstantDeliveryDescription.visibility = View.GONE
|
else subscribeInstantDeliveryDescription.visibility = View.GONE
|
||||||
|
@ -303,7 +303,7 @@ class AddFragment : DialogFragment() {
|
||||||
Log.w(TAG, "Access not allowed to topic ${topicUrl(baseUrl, topic)}, showing login dialog")
|
Log.w(TAG, "Access not allowed to topic ${topicUrl(baseUrl, topic)}, showing login dialog")
|
||||||
val activity = activity ?: return@launch // We may have pressed "Cancel"
|
val activity = activity ?: return@launch // We may have pressed "Cancel"
|
||||||
activity.runOnUiThread {
|
activity.runOnUiThread {
|
||||||
showLoginView(activity, baseUrl)
|
showLoginView(activity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -444,7 +444,7 @@ class AddFragment : DialogFragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showLoginView(activity: Activity, baseUrl: String) {
|
private fun showLoginView(activity: Activity) {
|
||||||
resetLoginView()
|
resetLoginView()
|
||||||
loginProgress.visibility = View.INVISIBLE
|
loginProgress.visibility = View.INVISIBLE
|
||||||
positiveButton.text = getString(R.string.add_dialog_button_login)
|
positiveButton.text = getString(R.string.add_dialog_button_login)
|
||||||
|
|
|
@ -14,15 +14,23 @@ import android.widget.ImageView
|
||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
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.Log
|
import io.heckel.ntfy.util.Log
|
||||||
|
import io.heckel.ntfy.util.supportedImage
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class ShareActivity : AppCompatActivity() {
|
class ShareActivity : AppCompatActivity() {
|
||||||
private val repository by lazy { (application as Application).repository }
|
private val repository by lazy { (application as Application).repository }
|
||||||
private val api = ApiService()
|
private val api = ApiService()
|
||||||
|
|
||||||
|
// File to share
|
||||||
|
private var fileUri: Uri? = null
|
||||||
|
|
||||||
// UI elements
|
// UI elements
|
||||||
private lateinit var menu: Menu
|
private lateinit var menu: Menu
|
||||||
private lateinit var sendItem: MenuItem
|
private lateinit var sendItem: MenuItem
|
||||||
|
@ -30,6 +38,8 @@ class ShareActivity : AppCompatActivity() {
|
||||||
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
|
||||||
|
private lateinit var errorText: TextView
|
||||||
|
private lateinit var errorImage: ImageView
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
@ -50,6 +60,10 @@ class ShareActivity : AppCompatActivity() {
|
||||||
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
|
||||||
|
errorText = findViewById(R.id.share_error_text)
|
||||||
|
errorText.visibility = View.GONE
|
||||||
|
errorImage = findViewById(R.id.share_error_image)
|
||||||
|
errorImage.visibility = View.GONE
|
||||||
|
|
||||||
val textWatcher = object : TextWatcher {
|
val textWatcher = object : TextWatcher {
|
||||||
override fun afterTextChanged(s: Editable?) {
|
override fun afterTextChanged(s: Editable?) {
|
||||||
|
@ -62,6 +76,7 @@ class ShareActivity : AppCompatActivity() {
|
||||||
// Nothing
|
// Nothing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
contentText.addTextChangedListener(textWatcher)
|
||||||
topicText.addTextChangedListener(textWatcher)
|
topicText.addTextChangedListener(textWatcher)
|
||||||
|
|
||||||
// Incoming intent
|
// Incoming intent
|
||||||
|
@ -69,8 +84,10 @@ class ShareActivity : AppCompatActivity() {
|
||||||
if (intent.action != Intent.ACTION_SEND) return
|
if (intent.action != Intent.ACTION_SEND) return
|
||||||
if ("text/plain" == intent.type) {
|
if ("text/plain" == intent.type) {
|
||||||
handleSendText(intent)
|
handleSendText(intent)
|
||||||
} else if (intent.type?.startsWith("image/") == true) {
|
} else if (supportedImage(intent.type)) {
|
||||||
handleSendImage(intent)
|
handleSendImage(intent)
|
||||||
|
} else {
|
||||||
|
handleSendFile(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,19 +99,26 @@ class ShareActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleSendImage(intent: Intent) {
|
private fun handleSendImage(intent: Intent) {
|
||||||
val uri = intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri ?: return
|
fileUri = intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri ?: return
|
||||||
try {
|
try {
|
||||||
val resolver = applicationContext.contentResolver
|
val resolver = applicationContext.contentResolver
|
||||||
val bitmapStream = resolver.openInputStream(uri)
|
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
|
contentImage.visibility = View.VISIBLE
|
||||||
contentText.text = getString(R.string.share_content_image_text)
|
contentText.text = getString(R.string.share_content_image_text)
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
|
fileUri = null
|
||||||
|
contentImage.visibility = View.GONE
|
||||||
contentText.text = getString(R.string.share_content_image_error)
|
contentText.text = getString(R.string.share_content_image_error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleSendFile(intent: Intent) {
|
||||||
|
fileUri = intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri ?: return
|
||||||
|
contentText.text = getString(R.string.share_content_file_text)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onSupportNavigateUp(): Boolean {
|
override fun onSupportNavigateUp(): Boolean {
|
||||||
finish()
|
finish()
|
||||||
return true
|
return true
|
||||||
|
@ -119,7 +143,42 @@ class ShareActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onShareClick() {
|
private fun onShareClick() {
|
||||||
|
val baseUrl = "https://ntfy.sh" // FIXME
|
||||||
|
val topic = topicText.text.toString()
|
||||||
|
val message = contentText.text.toString()
|
||||||
|
progress.visibility = View.VISIBLE
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
val user = repository.getUser(baseUrl)
|
||||||
|
try {
|
||||||
|
val body = if (fileUri != null) {
|
||||||
|
val resolver = applicationContext.contentResolver
|
||||||
|
ContentUriRequestBody(resolver, fileUri!!)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
api.publish(
|
||||||
|
baseUrl = baseUrl,
|
||||||
|
topic = topic,
|
||||||
|
user = user,
|
||||||
|
message = message,
|
||||||
|
title = "",
|
||||||
|
priority = 3,
|
||||||
|
tags = emptyList(),
|
||||||
|
delay = "",
|
||||||
|
body = body // May be null
|
||||||
|
)
|
||||||
|
runOnUiThread {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
runOnUiThread {
|
||||||
|
progress.visibility = View.GONE
|
||||||
|
errorText.text = e.message
|
||||||
|
errorImage.visibility = View.VISIBLE
|
||||||
|
errorText.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun validateInput() {
|
private fun validateInput() {
|
||||||
|
|
|
@ -2,6 +2,7 @@ package io.heckel.ntfy.util
|
||||||
|
|
||||||
import android.animation.ArgbEvaluator
|
import android.animation.ArgbEvaluator
|
||||||
import android.animation.ValueAnimator
|
import android.animation.ValueAnimator
|
||||||
|
import android.content.ContentResolver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
||||||
|
@ -14,6 +15,12 @@ import androidx.appcompat.app.AppCompatDelegate
|
||||||
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
|
||||||
|
import okhttp3.MediaType
|
||||||
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
|
import okhttp3.RequestBody
|
||||||
|
import okio.BufferedSink
|
||||||
|
import okio.source
|
||||||
|
import java.io.IOException
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
import java.text.StringCharacterIterator
|
import java.text.StringCharacterIterator
|
||||||
|
@ -218,3 +225,22 @@ fun isDarkThemeOn(context: Context): Boolean {
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://cketti.de/2020/05/23/content-uris-and-okhttp/
|
||||||
|
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 ->
|
||||||
|
sink.writeAll(source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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_marginTop="5dp" android:layout_marginStart="5dp"/>
|
app:layout_constraintTop_toBottomOf="@id/share_content_title" android:layout_marginStart="3dp"/>
|
||||||
<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"
|
||||||
|
@ -58,5 +58,21 @@
|
||||||
android:maxLines="1" android:inputType="text" android:maxLength="64"
|
android:maxLines="1" android:inputType="text" android:maxLength="64"
|
||||||
app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/share_topic_title"/>
|
app:layout_constraintTop_toBottomOf="@id/share_topic_title"/>
|
||||||
|
<TextView
|
||||||
|
android:text="Unable to resolve host example.com"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content" android:id="@+id/share_error_text"
|
||||||
|
android:paddingStart="4dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
android:paddingEnd="4dp"
|
||||||
|
android:textAppearance="@style/DangerText"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/share_error_image"
|
||||||
|
android:layout_marginTop="5dp" app:layout_constraintTop_toBottomOf="@+id/share_topic_text"/>
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="20dp"
|
||||||
|
android:layout_height="20dp" app:srcCompat="@drawable/ic_error_red_24dp"
|
||||||
|
android:id="@+id/share_error_image"
|
||||||
|
android:visibility="visible"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@+id/share_error_text" android:layout_marginTop="2dp"/>
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
|
@ -190,8 +190,9 @@
|
||||||
<string name="share_menu_send">Share</string>
|
<string name="share_menu_send">Share</string>
|
||||||
<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</string>
|
||||||
|
<string name="share_content_file_text">An file was shared with you</string>
|
||||||
|
|
||||||
<!-- Notification dialog -->
|
<!-- Notification dialog -->
|
||||||
<string name="notification_dialog_title">Pause notifications</string>
|
<string name="notification_dialog_title">Pause notifications</string>
|
||||||
|
|
Loading…
Reference in a new issue