Skip to content

Commit

Permalink
Merge branch 'improve-sharing' into v1.9-dev
Browse files Browse the repository at this point in the history
  • Loading branch information
oakkitten committed Jan 28, 2024
2 parents 1517beb + 0ea489d commit 8d3f959
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 49 deletions.
5 changes: 1 addition & 4 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ dependencies {
implementation(project(":cats"))
implementation(project(":relay"))

implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.0")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.20")

// these two are required for logging within the relay module. todo remove?
implementation("org.slf4j:slf4j-api:1.7.36")
Expand Down Expand Up @@ -87,9 +87,6 @@ android {
}

kotlinOptions {
freeCompilerArgs = listOf(
"-language-version", "1.7",
"-api-version", "1.7")
jvmTarget = "11"
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import android.os.Build
import android.os.Bundle
import android.transition.Fade
import android.transition.TransitionManager
import android.view.DragEvent
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.Menu
Expand All @@ -28,6 +29,7 @@ import androidx.core.view.MenuCompat
import androidx.core.view.forEach
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ItemDecoration
import com.ubergeek42.WeechatAndroid.R
Expand All @@ -54,9 +56,11 @@ import com.ubergeek42.WeechatAndroid.upload.MediaAcceptingEditText.HasLayoutList
import com.ubergeek42.WeechatAndroid.upload.ShareObject
import com.ubergeek42.WeechatAndroid.upload.Suri
import com.ubergeek42.WeechatAndroid.upload.Target
import com.ubergeek42.WeechatAndroid.upload.TextShareObject
import com.ubergeek42.WeechatAndroid.upload.Upload.CancelledException
import com.ubergeek42.WeechatAndroid.upload.UploadManager
import com.ubergeek42.WeechatAndroid.upload.UploadObserver
import com.ubergeek42.WeechatAndroid.upload.UrisShareObject
import com.ubergeek42.WeechatAndroid.upload.WRITE_PERMISSION_REQUEST_FOR_CAMERA
import com.ubergeek42.WeechatAndroid.upload.chooseFiles
import com.ubergeek42.WeechatAndroid.upload.getShareObjectFromIntent
Expand All @@ -80,6 +84,7 @@ import com.ubergeek42.cats.CatD
import com.ubergeek42.cats.Kitty
import com.ubergeek42.cats.Root
import com.ubergeek42.weechat.ColorScheme
import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
Expand Down Expand Up @@ -222,6 +227,9 @@ class BufferFragment : Fragment(), BufferEye {
}
}

ui.chatInput.setOnDragListener(onDragListener)
ui.root.setOnDragListener(onDragListener)

ui.scrollToBottomFab.setOnClickListener {
ui.chatLines.jumpThenSmoothScroll(linesAdapter!!.itemCount - 1)
focusedMatch = 0
Expand Down Expand Up @@ -972,6 +980,46 @@ class BufferFragment : Fragment(), BufferEye {
pendingInputs.remove(buffer.fullName)
}
}

// It is possible to use OnReceiveContentListener instead of this.
// This allows showing some drag and drop indications,
// as well as more explicit and possibly simpler permission handling.
// We indicate that we handle `ACTION_DRAG_STARTED`, else `ACTION_DROP` is not received;
// We indicate that we don't handle other events
// in order to allow cursor movement in the input field while dragging.
//
// We are saving input for parallel fragments (i.e. bubbles) on pause and restoring it on resume.
// On some systems, particularly on API 27, it's possible that, when dragging from another app,
// the target activity isn't actually resumed, hence this input change may fail to be recorded.
// To prevent this, explicitly record the change after loading the thumbnails.
private val onDragListener = View.OnDragListener { _, event ->
if (event.action == DragEvent.ACTION_DRAG_STARTED) return@OnDragListener true
if (event.action != DragEvent.ACTION_DROP) return@OnDragListener false

ulet(activity, ui, event.clipData) { activity, ui, clipData ->
val uris = (0..<clipData.itemCount).mapNotNull { clipData.getItemAt(it).uri }

if (uris.isNotEmpty()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
activity.requestDragAndDropPermissions(event)
}

lifecycleScope.launch {
suppress<Exception>(showToast = true) {
UrisShareObject.fromUris(uris).insertAsync(ui.chatInput, InsertAt.CURRENT_POSITION)
setPendingInputForParallelFragments()
}
}
} else if (clipData.itemCount > 0) {
clipData.getItemAt(0).text?.let { text ->
TextShareObject(text).insert(ui.chatInput, InsertAt.CURRENT_POSITION)
setPendingInputForParallelFragments()
}
}
}

true
}
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,18 @@ import android.net.Uri
import android.os.Build
import android.os.Parcel
import android.os.Parcelable
import android.text.Spanned
import android.util.AttributeSet
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputConnection
import androidx.core.view.inputmethod.EditorInfoCompat
import androidx.core.view.inputmethod.InputConnectionCompat
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.ubergeek42.WeechatAndroid.utils.ActionEditText
import com.ubergeek42.WeechatAndroid.utils.Toaster.Companion.ErrorToast
import com.ubergeek42.cats.Kitty
import com.ubergeek42.cats.Root
import kotlinx.coroutines.launch


class MediaAcceptingEditText : ActionEditText {
Expand Down Expand Up @@ -67,8 +69,10 @@ class MediaAcceptingEditText : ActionEditText {
text?.let {
for (span in it.getSpans(0, it.length, ShareSpan::class.java)) {
span.suri.httpUri?.let { httpUri ->
it.replace(it.getSpanStart(span), it.getSpanEnd(span), httpUri)
val pos = it.getSpanStart(span)
it.replace(pos, it.getSpanEnd(span), "")
it.removeSpan(span)
insertAddingSpacesAsNeeded(pos, httpUri)
}
}
}
Expand Down Expand Up @@ -107,7 +111,7 @@ class MediaAcceptingEditText : ActionEditText {
///////////////////////////////////////////////////////////////////////////////// save & restore
////////////////////////////////////////////////////////////////////////////////////////////////

class ShareSpanInfo(val uri: Uri, val start: Int, val end: Int)
data class ShareSpanInfo(val uri: Uri, val start: Int, val end: Int)

override fun onSaveInstanceState(): Parcelable? {
return SavedState(super.onSaveInstanceState()).apply {
Expand All @@ -127,14 +131,13 @@ class MediaAcceptingEditText : ActionEditText {
// if so, make sure we don't create them twice
if (state.shareSpans.isEmpty() || text == null || hasShareSpans()) return

state.shareSpans.forEach {
suppress<Exception>(showToast = true) {
val suri = Suri.fromUri(it.uri)
getThumbnailAndThen(context, it.uri) { bitmap ->
val span = if (bitmap != NO_BITMAP)
BitmapShareSpan(suri, bitmap) else NonBitmapShareSpan(suri)
findViewTreeLifecycleOwner()?.lifecycleScope?.launch {
state.shareSpans.forEach { (uri, start, end) ->
launch {
suppress<Exception>(showToast = true) {
text?.setSpan(span, it.start, it.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
val suri = Suri.fromUri(uri)
val thumbnailSpannable = makeThumbnailSpannable(context, suri)
text?.replace(start, end, thumbnailSpannable)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import android.net.Uri
import android.text.*
import android.widget.EditText
import androidx.annotation.MainThread
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.bumptech.glide.Glide
import com.bumptech.glide.load.MultiTransformation
import com.bumptech.glide.load.engine.DiskCacheStrategy
Expand All @@ -16,8 +18,15 @@ import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import com.ubergeek42.WeechatAndroid.media.Config
import com.ubergeek42.WeechatAndroid.media.WAGlideModule.isContextValidForGlide
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import java.io.FileNotFoundException
import java.io.IOException
import kotlin.coroutines.resume


enum class InsertAt {
Expand Down Expand Up @@ -45,32 +54,20 @@ const val PLACEHOLDER_TEXT = "\u00a0"
open class UrisShareObject(
private val suris: List<Suri>
) : ShareObject {
protected val bitmaps: Array<Bitmap?> = arrayOfNulls(suris.size)

override fun insert(editText: EditText, insertAt: InsertAt) {
val context = editText.context
getAllImagesAndThen(context) {
for (i in suris.indices) {
editText.insertAddingSpacesAsNeeded(insertAt, makeImageSpanned(i))
}
editText.findViewTreeLifecycleOwner()?.lifecycleScope?.launch {
insertAsync(editText, insertAt)
}
}

open fun getAllImagesAndThen(context: Context, then: () -> Unit) {
suris.forEachIndexed { i, suri ->
getThumbnailAndThen(context, suri.uri) { bitmap ->
bitmaps[i] = bitmap
if (bitmaps.all { it != null }) then()
}
suspend fun insertAsync(editText: EditText, insertAt: InsertAt) {
val thumbnailSpannables = coroutineScope {
suris.map { async { makeThumbnailSpannable(editText.context, it) } }.awaitAll()
}
}

private fun makeImageSpanned(i: Int) : Spanned {
val spanned = SpannableString(PLACEHOLDER_TEXT)
val imageSpan = if (bitmaps[i] != NO_BITMAP)
BitmapShareSpan(suris[i], bitmaps[i]!!) else NonBitmapShareSpan(suris[i])
spanned.setSpan(imageSpan, 0, spanned.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
return spanned
thumbnailSpannables.forEach { thumbnailSpannable ->
editText.insertAddingSpacesAsNeeded(insertAt, thumbnailSpannable)
}
}

companion object {
Expand All @@ -81,15 +78,23 @@ open class UrisShareObject(
}
}


////////////////////////////////////////////////////////////////////////////////////////////////////

suspend fun makeThumbnailSpannable(context: Context, suri: Suri): Spannable {
val bitmapOrNull = getBitmapOrNull(context, suri.uri)
val span = if (bitmapOrNull != null) BitmapShareSpan(suri, bitmapOrNull) else NonBitmapShareSpan(suri)

return SpannableString(PLACEHOLDER_TEXT).apply {
setSpan(span, 0, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}

// this starts the upload in a worker thread and exits immediately.
// target callbacks will be called on the main thread
fun getThumbnailAndThen(context: Context, uri: Uri, then: (bitmap: Bitmap) -> Unit) {
if (isContextValidForGlide(context)) {
Glide.with(context)
suspend fun getBitmapOrNull(context: Context, uri: Uri): Bitmap? =
suspendCancellableCoroutine { continuation ->
if (isContextValidForGlide(context)) {
Glide.with(context)
.asBitmap()
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.transform(MultiTransformation(
Expand All @@ -99,28 +104,32 @@ fun getThumbnailAndThen(context: Context, uri: Uri, then: (bitmap: Bitmap) -> Un
.load(uri)
.into(object : CustomTarget<Bitmap>(THUMBNAIL_MAX_WIDTH, THUMBNAIL_MAX_HEIGHT) {
@MainThread override fun onResourceReady(bitmap: Bitmap, transition: Transition<in Bitmap>?) {
then(bitmap)
continuation.resume(bitmap)
}

// The request seems to be attempted once again on minimizing/restoring the app.
// To avoid that, clear target soon, but not on current thread--the library doesn't allow it.
// See https://github.com/bumptech/glide/issues/4125
@MainThread override fun onLoadFailed(errorDrawable: Drawable?) {
then(NO_BITMAP)
continuation.resume(null)
main { Glide.with(context).clear(this) }
}

override fun onLoadCleared(placeholder: Drawable?) {
// this shouldn't happen
}
})
}
}
}

val NO_BITMAP: Bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ALPHA_8)

////////////////////////////////////////////////////////////////////////////////////////////////////


@MainThread fun EditText.insertAddingSpacesAsNeeded(insertAt: InsertAt, word: CharSequence) {
val pos = if (insertAt == InsertAt.CURRENT_POSITION) selectionEnd else text.length
insertAddingSpacesAsNeeded(pos, word)
}

@MainThread fun EditText.insertAddingSpacesAsNeeded(pos: Int, word: CharSequence) {
val wordStartsWithSpace = word.firstOrNull() == ' '
val wordEndsWithSpace = word.lastOrNull() == ' '
val spaceBeforeInsertLocation = pos > 0 && text[pos - 1] == ' '
Expand Down Expand Up @@ -161,9 +170,7 @@ fun preloadThumbnailsForIntent(intent: Intent) {
else -> null
}

if (uris != null) {
suppress<Exception> {
UrisShareObject.fromUris(uris).getAllImagesAndThen(applicationContext) {}
}
uris?.forEach { uri ->
GlobalScope.launch { getBitmapOrNull(applicationContext, uri) }
}
}
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ buildscript {
dependencies {
classpath("com.android.tools.build:gradle:7.3.1")
classpath("org.aspectj:aspectjtools:1.9.9.1")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.0")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.20")
classpath("org.jetbrains.kotlin:kotlin-serialization:1.7.0")
}
}
Expand Down

0 comments on commit 8d3f959

Please sign in to comment.