WIP: Share feature

This commit is contained in:
Philipp Heckel 2022-02-11 10:46:55 -05:00
parent 9ca5ebe6d2
commit 3b30e39eb5
9 changed files with 236 additions and 2 deletions

View file

@ -58,6 +58,20 @@
android:parentActivityName=".ui.DetailActivity"> android:parentActivityName=".ui.DetailActivity">
</activity> </activity>
<!-- Share file activity, incoming files/shares -->
<activity android:name=".ui.ShareActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
<!-- Subscriber foreground service for hosts other than ntfy.sh --> <!-- Subscriber foreground service for hosts other than ntfy.sh -->
<service android:name=".service.SubscriberService"/> <service android:name=".service.SubscriberService"/>

View file

@ -374,6 +374,7 @@ class AddFragment : DialogFragment() {
} }
private fun validateInputSubscribeView() { private fun validateInputSubscribeView() {
if (!this::positiveButton.isInitialized) return // As per crash seen in Google Play
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
val baseUrl = getBaseUrl() val baseUrl = getBaseUrl()
val topic = subscribeTopicText.text.toString() val topic = subscribeTopicText.text.toString()
@ -398,6 +399,7 @@ class AddFragment : DialogFragment() {
} }
private fun validateInputLoginView() { private fun validateInputLoginView() {
if (!this::positiveButton.isInitialized) return // As per crash seen in Google Play
if (loginUsernameText.visibility == View.GONE) { if (loginUsernameText.visibility == View.GONE) {
positiveButton.isEnabled = true positiveButton.isEnabled = true
} else { } else {

View file

@ -0,0 +1,134 @@
package io.heckel.ntfy.ui
import android.content.Intent
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Bundle
import android.os.Parcelable
import android.text.Editable
import android.text.TextWatcher
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import io.heckel.ntfy.R
import io.heckel.ntfy.app.Application
import io.heckel.ntfy.msg.ApiService
import io.heckel.ntfy.util.Log
class ShareActivity : AppCompatActivity() {
private val repository by lazy { (application as Application).repository }
private val api = ApiService()
// UI elements
private lateinit var menu: Menu
private lateinit var sendItem: MenuItem
private lateinit var contentImage: ImageView
private lateinit var contentText: TextView
private lateinit var topicText: TextView
private lateinit var progress: ProgressBar
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_share)
Log.init(this) // Init logs in all entry points
Log.d(TAG, "Create $this")
// Action bar
title = getString(R.string.share_title)
// Show 'Back' button
supportActionBar?.setDisplayHomeAsUpEnabled(true)
// UI elements
contentText = findViewById(R.id.share_content_text)
contentImage = findViewById(R.id.share_content_image)
topicText = findViewById(R.id.share_topic_text)
progress = findViewById(R.id.share_progress)
progress.visibility = View.GONE
val textWatcher = object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
validateInput()
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
// Nothing
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
// Nothing
}
}
topicText.addTextChangedListener(textWatcher)
// Incoming intent
val intent = intent ?: return
if (intent.action != Intent.ACTION_SEND) return
if ("text/plain" == intent.type) {
handleSendText(intent)
} else if (intent.type?.startsWith("image/") == true) {
handleSendImage(intent)
}
}
private fun handleSendText(intent: Intent) {
intent.getStringExtra(Intent.EXTRA_TEXT)?.let { text ->
contentImage.visibility = View.GONE
contentText.text = text
}
}
private fun handleSendImage(intent: Intent) {
val uri = intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri ?: return
try {
val resolver = applicationContext.contentResolver
val bitmapStream = resolver.openInputStream(uri)
val bitmap = BitmapFactory.decodeStream(bitmapStream)
contentImage.setImageBitmap(bitmap)
contentImage.visibility = View.VISIBLE
contentText.text = getString(R.string.share_content_image_text)
} catch (_: Exception) {
contentText.text = getString(R.string.share_content_image_error)
}
}
override fun onSupportNavigateUp(): Boolean {
finish()
return true
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_share_action_bar, menu)
this.menu = menu
sendItem = menu.findItem(R.id.share_menu_send)
validateInput() // Disable icon
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.share_menu_send -> {
onShareClick()
true
}
else -> super.onOptionsItemSelected(item)
}
}
private fun onShareClick() {
}
private fun validateInput() {
if (!this::sendItem.isInitialized) return // Initialized late in onCreateOptionsMenu
sendItem.isEnabled = contentText.text.isNotEmpty() && topicText.text.isNotEmpty()
sendItem.icon.alpha = if (sendItem.isEnabled) 255 else 130
}
companion object {
const val TAG = "NtfyShareActivity"
}
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M4.01,6.03l7.51,3.22 -7.52,-1 0.01,-2.22m7.5,8.72L4,17.97v-2.22l7.51,-1M2.01,3L2,10l15,2 -15,2 0.01,7L23,12 2.01,3z"
android:fillColor="#FFFFFF"/>
</vector>

View file

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
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">
<ProgressBar
style="?android:attr/progressBarStyle"
android:layout_width="24dp"
android:layout_height="24dp"
android:id="@+id/share_progress"
app:layout_constraintEnd_toEndOf="parent"
android:indeterminate="true" android:visibility="visible" app:layout_constraintTop_toTopOf="parent" android:layout_marginTop="5dp"/>
<TextView
android:id="@+id/share_content_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingTop="5dp"
android:paddingBottom="2dp"
android:text="@string/share_content_title"
android:textAlignment="viewStart"
android:textAppearance="@style/TextAppearance.AppCompat.Medium" android:paddingStart="4dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<com.google.android.material.imageview.ShapeableImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content" app:srcCompat="@drawable/ic_cancel_gray_24dp"
android:id="@+id/share_content_image" app:layout_constraintStart_toStartOf="parent"
android:scaleType="fitStart"
android:adjustViewBounds="true" android:maxHeight="150dp"
app:shapeAppearanceOverlay="@style/roundedCornersImageView" android:visibility="visible"
app:layout_constraintTop_toBottomOf="@id/share_content_title" android:layout_marginTop="5dp" android:layout_marginStart="5dp"/>
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/share_content_text"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:hint="@string/share_content_text_hint"
android:importantForAutofill="no"
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"/>
<TextView
android:id="@+id/share_topic_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingTop="5dp"
android:paddingBottom="3dp"
android:text="Share to topic"
android:textAlignment="viewStart"
android:textAppearance="@style/TextAppearance.AppCompat.Medium" android:paddingStart="4dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/share_content_text" android:layout_marginTop="10dp"/>
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/share_topic_text"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:hint="@string/add_dialog_topic_name_hint"
android:importantForAutofill="no"
android:maxLines="1" android:inputType="text" android:maxLength="64"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/share_topic_title"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -15,7 +15,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal"> android:orientation="horizontal">
<TextView <TextView
android:id="@+id/add_dialog_subscribe_title_text" android:id="@+id/add_dialog_subscribe_title"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingTop="16dp" android:paddingTop="16dp"
@ -41,7 +41,7 @@
android:layout_height="wrap_content" android:id="@+id/add_dialog_subscribe_description" android:layout_height="wrap_content" android:id="@+id/add_dialog_subscribe_description"
android:paddingStart="4dp" android:paddingTop="3dp" app:layout_constraintStart_toStartOf="parent" android:paddingStart="4dp" android:paddingTop="3dp" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/add_dialog_subscribe_title_text"/> app:layout_constraintTop_toBottomOf="@id/add_dialog_subscribe_title"/>
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/add_dialog_subscribe_topic_text" android:id="@+id/add_dialog_subscribe_topic_text"
android:layout_width="match_parent" android:layout_width="match_parent"

View file

@ -0,0 +1,4 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/share_menu_send" android:title="@string/share_menu_send"
app:showAsAction="always" android:icon="@drawable/ic_send_white_24dp"/>
</menu>

View file

@ -185,6 +185,14 @@
<string name="detail_settings_title">Subscription settings</string> <string name="detail_settings_title">Subscription settings</string>
<!-- ... --> <!-- ... -->
<!-- Share activity -->
<string name="share_title">Share</string>
<string name="share_menu_send">Share</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_image_text">An image was shared with you.</string>
<string name="share_content_image_error">Cannot read image</string>
<!-- Notification dialog --> <!-- Notification dialog -->
<string name="notification_dialog_title">Pause notifications</string> <string name="notification_dialog_title">Pause notifications</string>
<string name="notification_dialog_cancel">Cancel</string> <string name="notification_dialog_cancel">Cancel</string>

View file

@ -4,3 +4,4 @@ Features:
Bug fixes: Bug fixes:
* Do not attempt to download attachments if they are already expired (#135) * Do not attempt to download attachments if they are already expired (#135)
* Fixed crash in AddFragment as seen per stack trace in Play Console (no ticket)