Skip to content

Commit

Permalink
changes to add a new setting that allows user to choose to allow all …
Browse files Browse the repository at this point in the history
…message links, block all message links (including in notifications), or ask via a dialog box whether the user wishes to proceed
  • Loading branch information
gavine99 committed Feb 2, 2025
1 parent f32e0b4 commit d7514d6
Show file tree
Hide file tree
Showing 13 changed files with 199 additions and 48 deletions.
5 changes: 5 additions & 0 deletions domain/src/main/java/com/moez/QKSMS/util/Preferences.kt
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ class Preferences @Inject constructor(
const val BLOCKING_MANAGER_CC = 1
const val BLOCKING_MANAGER_SIA = 2
const val BLOCKING_MANAGER_CB = 3

const val MESSAGE_LINK_HANDLING_BLOCK = 0
const val MESSAGE_LINK_HANDLING_ALLOW = 1
const val MESSAGE_LINK_HANDLING_ASK = 2
}

// Internal
Expand Down Expand Up @@ -124,6 +128,7 @@ class Preferences @Inject constructor(
val autoDelete = rxPrefs.getInteger("autoDelete", 0)
val longAsMms = rxPrefs.getBoolean("longAsMms", false)
val mmsSize = rxPrefs.getInteger("mmsSize", 300)
val messageLinkHandling = rxPrefs.getInteger("messageLinkHandling", MESSAGE_LINK_HANDLING_ASK)
val logging = rxPrefs.getBoolean("logging", false)
val unreadAtTop = rxPrefs.getBoolean("unreadAtTop", false)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,12 @@ class NotificationManagerImpl @Inject constructor(
}
}

// remove system generated contextual actions (they're generated from message links) unless
// the user has explicitly set to allow message links
notification.setAllowSystemGeneratedContextualActions(
(prefs.messageLinkHandling.get() == Preferences.MESSAGE_LINK_HANDLING_ALLOW)
)

// Set the large icon
val avatar = conversation.recipients.takeIf { it.size == 1 }
?.first()?.contact?.photoUri
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,55 +92,67 @@ fun View.setVisible(visible: Boolean, invisible: Int = View.GONE) {
* the view no longer work. Also problematic when we try to long press on an image in the message
* view
*/
fun View.forwardTouches(parent: View) {
var isLongClick = false
var textInitiallySelectable = false
if (this@forwardTouches is TextView)
textInitiallySelectable = this@forwardTouches.isTextSelectable

class CancelableSimpleOnGestureListener(view: View, parentView: View) : SimpleOnGestureListener() {
private var lastUpEvent: MotionEvent? = null
private val parent = parentView
private val thisView = view
private var textInitiallySelectable = false

init {
if (thisView is TextView)
textInitiallySelectable = thisView.isTextSelectable
}

fun cancelCurrentClick() {
lastUpEvent?.recycle()
lastUpEvent = null
}

override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
if (lastUpEvent !== null) {
parent.onTouchEvent(e)
parent.onTouchEvent(lastUpEvent)
lastUpEvent?.recycle()
lastUpEvent = null
}
return true
}

override fun onSingleTapUp(e: MotionEvent): Boolean {
lastUpEvent = MotionEvent.obtain(e)
thisView.onTouchEvent(e)
return true
}

override fun onDown(e: MotionEvent): Boolean {
thisView.onTouchEvent(e)
return true
}

override fun onLongPress(e: MotionEvent) {
parent.onTouchEvent(e)
// this is kinda odd but we have to 'bounce' the text selectable value so it doesn't
// start selecting text on a long press, but will start selecting it on the next double-tap
if (thisView is TextView) {
thisView.setTextIsSelectable(false)
thisView.setTextIsSelectable(textInitiallySelectable)
}
}
}

fun View.forwardTouches(parent: View): CancelableSimpleOnGestureListener {
val gestureListener = CancelableSimpleOnGestureListener(this, parent)

setOnTouchListener(object : OnTouchListener {
private val gestureDetector =
GestureDetector(parent.context, object : SimpleOnGestureListener() {
private var lastUpEvent: MotionEvent? = null

override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
parent.onTouchEvent(e)
if (lastUpEvent !== null) {
parent.onTouchEvent(lastUpEvent)
lastUpEvent?.recycle()
lastUpEvent = null
}
return true
}

override fun onSingleTapUp(e: MotionEvent): Boolean {
onTouchEvent(e)
lastUpEvent = MotionEvent.obtain(e)
return true
}

override fun onDown(e: MotionEvent): Boolean {
isLongClick = false
onTouchEvent(e)
return true
}

override fun onLongPress(e: MotionEvent) {
parent.onTouchEvent(e)
isLongClick = true
// this is kinda odd but we have to 'bounce' the text selectable value so it doesn't
// start selecting text on a long press, but will start selecting it on the next double-tap
if (this@forwardTouches is TextView) {
(this@forwardTouches as TextView).setTextIsSelectable(false)
(this@forwardTouches as TextView).setTextIsSelectable(textInitiallySelectable)
}
}
})
val gestureDetector = GestureDetector(parent.context, gestureListener)

override fun onTouch(v: View, e: MotionEvent): Boolean {
return gestureDetector.onTouchEvent(e)
}
})

return gestureListener
}

fun ViewPager.addOnPageChangeListener(listener: (Int) -> Unit) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import androidx.appcompat.app.AlertDialog
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProviders
Expand Down Expand Up @@ -121,6 +122,7 @@ class ComposeActivity : QkThemedActivity(), ComposeView {
override val viewQksmsPlusIntent: Subject<Unit> = PublishSubject.create()
override val backPressedIntent: Subject<Unit> = PublishSubject.create()
override val confirmDeleteIntent: Subject<List<Long>> = PublishSubject.create()
override val messageLinkAskIntent: Subject<Uri> by lazy { messageAdapter.messageLinkClicks }

private val viewModel by lazy { ViewModelProviders.of(this, viewModelFactory)[ComposeViewModel::class.java] }

Expand Down Expand Up @@ -350,6 +352,23 @@ class ComposeActivity : QkThemedActivity(), ComposeView {
.show()
}

override fun showMessageLinkAskDialog(uri: Uri) {
AlertDialog.Builder(this)
.setTitle(R.string.messageLinkHandling_dialog_title)
.setMessage(getString(R.string.messageLinkHandling_dialog_body, uri.toString()))
.setPositiveButton(
R.string.messageLinkHandling_dialog_positive
) { _, _ ->
ContextCompat.startActivity(
this,
Intent(Intent.ACTION_VIEW).setData(uri),
null
)
}
.setNegativeButton(R.string.messageLinkHandling_dialog_negative) { _, _ -> { } }
.show()
}

override fun requestDefaultSms() {
navigator.showDefaultSmsDialog(this)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,13 @@ interface ComposeView : QkView<ComposeState> {
val viewQksmsPlusIntent: Subject<Unit>
val backPressedIntent: Observable<Unit>
val confirmDeleteIntent: Observable<List<Long>>
val messageLinkAskIntent: Observable<Uri>

fun clearSelection()
fun toggleSelectAll()
fun expandMessages(messageIds: List<Long>, expand: Boolean)
fun showDetails(details: String)
fun showMessageLinkAskDialog(uri: Uri)
fun requestDefaultSms()
fun requestStoragePermission()
fun requestSmsPermission()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,11 @@ class ComposeViewModel @Inject constructor(
)
}

// Show the message details
view.messageLinkAskIntent
.autoDisposable(view.scope())
.subscribe { view.showMessageLinkAskDialog(it) }

// Set the current conversation
Observables
.combineLatest(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,21 @@ package dev.octoshrimpy.quik.feature.compose
import android.animation.ObjectAnimator
import android.content.Context
import android.graphics.Typeface
import android.os.Build
import android.net.Uri
import android.text.Layout
import android.text.Spannable
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.text.method.LinkMovementMethod
import android.text.style.ClickableSpan
import android.text.style.StyleSpan
import android.text.style.URLSpan
import android.text.util.Linkify
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.ProgressBar
import androidx.recyclerview.widget.RecyclerView
import com.jakewharton.rxbinding2.view.clicks
import com.moez.QKSMS.common.QkMediaPlayer
import dev.octoshrimpy.quik.R
Expand Down Expand Up @@ -107,6 +111,7 @@ class MessagesAdapter @Inject constructor(

val clicks: Subject<Long> = PublishSubject.create()
val partClicks: Subject<Long> = PublishSubject.create()
val messageLinkClicks: Subject<Uri> = PublishSubject.create()
val cancelSending: Subject<Long> = PublishSubject.create()
val sendNow: Subject<Long> = PublishSubject.create()

Expand Down Expand Up @@ -170,7 +175,6 @@ class MessagesAdapter @Inject constructor(
val partsAdapter = partsAdapterProvider.get()
partsAdapter.clicks.subscribe(partClicks)
view.attachments.adapter = partsAdapter
view.body.forwardTouches(view)

return QkViewHolder(view).apply {
view.setOnClickListener {
Expand Down Expand Up @@ -240,6 +244,8 @@ class MessagesAdapter @Inject constructor(
}
}

val cancelableSimpleOnGestureListener = holder.body.forwardTouches(holder.containerView)

// Bind the message status
bindStatus(holder, message, next)

Expand Down Expand Up @@ -297,8 +303,47 @@ class MessagesAdapter @Inject constructor(
false -> TextViewStyler.SIZE_PRIMARY
})

holder.body.text = messageText
holder.body.setVisible(message.isSms() || messageText.isNotBlank())
val spanString = SpannableStringBuilder(messageText)

when (prefs.messageLinkHandling.get()) {
Preferences.MESSAGE_LINK_HANDLING_BLOCK -> holder.body.autoLinkMask = 0
Preferences.MESSAGE_LINK_HANDLING_ASK -> {
// manually handle link clicks if user has set to ask before opening links
holder.body.isClickable = false
holder.body.linksClickable = false
holder.body.movementMethod = LinkMovementMethod.getInstance()

Linkify.addLinks(spanString, holder.body.autoLinkMask)

for (span in spanString.getSpans(
0,
spanString.length,
URLSpan::class.java)
) {
// set handler for when user touches a link into new span
spanString.setSpan(
object : ClickableSpan() {
override fun onClick(widget: View) {
messageLinkClicks.onNext(Uri.parse(span.url))

// interrupt the upcoming click event on the body view
cancelableSimpleOnGestureListener.cancelCurrentClick()
}
},
spanString.getSpanStart(span),
spanString.getSpanEnd(span),
spanString.getSpanFlags(span)
)

// remove original span
spanString.removeSpan(span)
}
}
}

holder.body.text = spanString
holder.body.setVisible(message.isSms() || spanString.isNotBlank())

holder.body.setBackgroundResource(getBubble(
emojiOnly = emojiOnly,
canGroupWithPrevious = canGroup(message, previous) || media.isNotEmpty(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ import dev.octoshrimpy.quik.common.util.extensions.animateLayoutChanges
import dev.octoshrimpy.quik.common.util.extensions.setBackgroundTint
import dev.octoshrimpy.quik.common.util.extensions.setVisible
import dev.octoshrimpy.quik.common.widget.PreferenceView
import dev.octoshrimpy.quik.common.widget.QkSwitch
import dev.octoshrimpy.quik.common.widget.TextInputDialog
import dev.octoshrimpy.quik.feature.settings.about.AboutController
import dev.octoshrimpy.quik.feature.settings.autodelete.AutoDeleteDialog
Expand Down Expand Up @@ -73,6 +72,7 @@ class SettingsController : QkController<SettingsView, SettingsState, SettingsPre
@Inject lateinit var textSizeDialog: QkDialog
@Inject lateinit var sendDelayDialog: QkDialog
@Inject lateinit var mmsSizeDialog: QkDialog
@Inject lateinit var messageLinkHandlingDialog: QkDialog

@Inject override lateinit var presenter: SettingsPresenter

Expand Down Expand Up @@ -113,6 +113,7 @@ class SettingsController : QkController<SettingsView, SettingsState, SettingsPre
textSizeDialog.adapter.setData(R.array.text_sizes)
sendDelayDialog.adapter.setData(R.array.delayed_sending_labels)
mmsSizeDialog.adapter.setData(R.array.mms_sizes, R.array.mms_sizes_ids)
messageLinkHandlingDialog.adapter.setData(R.array.messageLinkHandlings, R.array.messageLinkHandling_ids)

about.summary = context.getString(R.string.settings_version, BuildConfig.VERSION_NAME)
}
Expand Down Expand Up @@ -150,6 +151,8 @@ class SettingsController : QkController<SettingsView, SettingsState, SettingsPre

override fun mmsSizeSelected(): Observable<Int> = mmsSizeDialog.adapter.menuItemClicks

override fun messageLinkHandlingSelected(): Observable<Int> = messageLinkHandlingDialog.adapter.menuItemClicks

override fun render(state: SettingsState) {
themePreview.setBackgroundTint(state.theme)
night.summary = state.nightModeSummary
Expand Down Expand Up @@ -195,6 +198,9 @@ class SettingsController : QkController<SettingsView, SettingsState, SettingsPre
mmsSize.summary = state.maxMmsSizeSummary
mmsSizeDialog.adapter.selectedItem = state.maxMmsSizeId

messsageLinkHandling.summary = state.messageLinkHandlingSummary
messageLinkHandlingDialog.adapter.selectedItem = state.messageLinkHandlingId

when (state.syncProgress) {
is SyncRepository.SyncProgress.Idle -> syncingProgress.isVisible = false

Expand Down Expand Up @@ -254,6 +260,8 @@ class SettingsController : QkController<SettingsView, SettingsState, SettingsPre

override fun showMmsSizePicker() = mmsSizeDialog.show(activity!!)

override fun showMessageLinkHandlingDialogPicker() = messageLinkHandlingDialog.show(activity!!)

override fun showSwipeActions() {
router.pushController(RouterTransaction.with(SwipeActionsController())
.pushChangeHandler(QkChangeHandler())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,19 @@ class SettingsPresenter @Inject constructor(
newState { copy(maxMmsSizeSummary = mmsSizeLabels[index], maxMmsSizeId = maxMmsSize) }
}

val messageLinkHandlingLabels = context.resources.getStringArray(R.array.messageLinkHandlings)
val messageLinkHandlingIds = context.resources.getIntArray(R.array.messageLinkHandling_ids)
disposables += prefs.messageLinkHandling.asObservable()
.subscribe { messageLinkHandlingId ->
val index = messageLinkHandlingIds.indexOf(messageLinkHandlingId)
newState {
copy(
messageLinkHandlingSummary = messageLinkHandlingLabels[index],
messageLinkHandlingId = messageLinkHandlingId
)
}
}

disposables += syncRepo.syncProgress
.sample(16, TimeUnit.MILLISECONDS)
.distinctUntilChanged()
Expand Down Expand Up @@ -204,6 +217,8 @@ class SettingsPresenter @Inject constructor(

R.id.mmsSize -> view.showMmsSizePicker()

R.id.messsageLinkHandling -> view.showMessageLinkHandlingDialogPicker()

R.id.sync -> syncMessages.execute(Unit)

R.id.about -> view.showAbout()
Expand Down Expand Up @@ -294,6 +309,10 @@ class SettingsPresenter @Inject constructor(
view.mmsSizeSelected()
.autoDisposable(view.scope())
.subscribe(prefs.mmsSize::set)

view.messageLinkHandlingSelected()
.autoDisposable(view.scope())
.subscribe(prefs.messageLinkHandling::set)
}

}
Loading

0 comments on commit d7514d6

Please sign in to comment.