From 042bbe80a1ff177484650c00415b41dad84a6ba4 Mon Sep 17 00:00:00 2001 From: gavine99 Date: Mon, 3 Feb 2025 19:58:40 +1100 Subject: [PATCH 1/2] changed to not save message attachments to media store when sharing. fixed using wrong attachment class for vcards (use lowercase on both sides of attachment mime type comparison) added share button to audio attachment preview removed old fileprovider that was never used added content provider to provide mms attachments to external activities using a filename - not mms part id - so they can be saved externally with a proper name fix bug when re-using attachment viewholder that has previously had text fixed bug of image attachments always being named 'image' rather than their filename added share button to generic file attachment type in compose message list and removed onclick handler for viewing --- .../QKSMS/extensions/MmsPartExtensions.kt | 12 +- .../QKSMS/repository/MessageRepositoryImpl.kt | 4 +- domain/src/main/AndroidManifest.xml | 12 +- .../QKSMS/contentproviders/MmsPartProvider.kt | 129 ++++++++++++++++++ .../main/java/com/moez/QKSMS/model/MmsPart.kt | 19 ++- presentation/src/main/AndroidManifest.xml | 11 -- .../feature/compose/AttachmentAdapter.kt | 2 + .../QKSMS/feature/compose/ComposeViewModel.kt | 12 +- .../QKSMS/feature/compose/part/AudioBinder.kt | 20 ++- .../QKSMS/feature/compose/part/FileBinder.kt | 19 ++- .../QKSMS/feature/gallery/GalleryViewModel.kt | 12 +- .../layout/mms_audio_preview_list_item.xml | 31 +++-- .../main/res/layout/mms_file_list_item.xml | 71 ++++++---- .../src/main/res/xml/provider_paths.xml | 24 ---- 14 files changed, 285 insertions(+), 93 deletions(-) create mode 100644 domain/src/main/java/com/moez/QKSMS/contentproviders/MmsPartProvider.kt delete mode 100644 presentation/src/main/res/xml/provider_paths.xml diff --git a/data/src/main/java/com/moez/QKSMS/extensions/MmsPartExtensions.kt b/data/src/main/java/com/moez/QKSMS/extensions/MmsPartExtensions.kt index e9c9ebac..64906b3b 100644 --- a/data/src/main/java/com/moez/QKSMS/extensions/MmsPartExtensions.kt +++ b/data/src/main/java/com/moez/QKSMS/extensions/MmsPartExtensions.kt @@ -21,14 +21,14 @@ package dev.octoshrimpy.quik.extensions import com.google.android.mms.ContentType import dev.octoshrimpy.quik.model.MmsPart -fun MmsPart.isSmil() = ContentType.APP_SMIL == type +fun MmsPart.isSmil() = ContentType.APP_SMIL.lowercase() == type.lowercase() -fun MmsPart.isImage() = ContentType.isImageType(type) +fun MmsPart.isImage() = ContentType.isImageType(type.lowercase()) -fun MmsPart.isVideo() = ContentType.isVideoType(type) +fun MmsPart.isVideo() = ContentType.isVideoType(type.lowercase()) -fun MmsPart.isAudio() = ContentType.isAudioType(type) +fun MmsPart.isAudio() = ContentType.isAudioType(type.lowercase()) -fun MmsPart.isText() = ContentType.TEXT_PLAIN == type +fun MmsPart.isText() = ContentType.TEXT_PLAIN.lowercase() == type.lowercase() -fun MmsPart.isVCard() = ContentType.TEXT_VCARD == type +fun MmsPart.isVCard() = ContentType.TEXT_VCARD.lowercase() == type.lowercase() diff --git a/data/src/main/java/com/moez/QKSMS/repository/MessageRepositoryImpl.kt b/data/src/main/java/com/moez/QKSMS/repository/MessageRepositoryImpl.kt index 5cb96793..5771cff3 100644 --- a/data/src/main/java/com/moez/QKSMS/repository/MessageRepositoryImpl.kt +++ b/data/src/main/java/com/moez/QKSMS/repository/MessageRepositoryImpl.kt @@ -443,8 +443,8 @@ class MessageRepositoryImpl @Inject constructor( imageBytesByAttachment.forEach { (attachment, bytes) -> parts += when (attachment.getType(context) == "image/gif") { - true -> MMSPart("image", ContentType.IMAGE_GIF, bytes) - false -> MMSPart("image", ContentType.IMAGE_JPEG, bytes) + true -> MMSPart(attachment.getName(context), ContentType.IMAGE_GIF, bytes) + false -> MMSPart(attachment.getName(context), ContentType.IMAGE_JPEG, bytes) } } diff --git a/domain/src/main/AndroidManifest.xml b/domain/src/main/AndroidManifest.xml index 21edd1fd..d70e29d4 100644 --- a/domain/src/main/AndroidManifest.xml +++ b/domain/src/main/AndroidManifest.xml @@ -16,4 +16,14 @@ ~ You should have received a copy of the GNU General Public License ~ along with QKSMS. If not, see . --> - + + + + + + diff --git a/domain/src/main/java/com/moez/QKSMS/contentproviders/MmsPartProvider.kt b/domain/src/main/java/com/moez/QKSMS/contentproviders/MmsPartProvider.kt new file mode 100644 index 00000000..84dd6bc3 --- /dev/null +++ b/domain/src/main/java/com/moez/QKSMS/contentproviders/MmsPartProvider.kt @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2017 Moez Bhatti + * + * This file is part of QKSMS. + * + * QKSMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QKSMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with QKSMS. If not, see . + */ +package com.moez.QKSMS.contentproviders + +import android.content.ContentProvider +import android.content.ContentResolver +import android.content.ContentValues +import android.database.Cursor +import android.net.Uri +import android.os.ParcelFileDescriptor +import java.io.FileNotFoundException + + +// the primary use case for this provider is to re-provide mms attachments but with a file name +// as the last part of the uri as opposed to the system mms provider uri which has a part id as +// it's 'filename' component. with this provider external applications can save/display using the +// original file name, if available, of the attachment when it is shared and/or saved +// it seems a needlessly complicated way to achieve the requirement, but it has to be done +class MmsPartProvider : ContentProvider() { + companion object { + private fun getMmsPartIdFromUri(uri: Uri): String? { + // ensure at least 2 path segments exist + if (uri.pathSegments.size < 2) + return null + + return try { + return uri.pathSegments[uri.pathSegments.size - 2] + } catch (e: Exception) { + null + } + } + + private fun getUnderlyingUriFromUri(uri: Uri): Uri { + val partId = getMmsPartIdFromUri(uri) ?: return Uri.EMPTY + return Uri + .Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority("mms") + .encodedPath("part/$partId") + .build() + } + + fun getUriForMmsPartId(partId: Long, partName: String): Uri { + return Uri + .Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority("dev.octoshrimpy.quik.mmspart") + .encodedPath("part/$partId/$partName") + .build() + } + } + + override fun onCreate(): Boolean { + return true + } + + override fun query( + uri: Uri, + projection: Array?, + selection: String?, + selectionArgs: Array?, + sortOrder: String? + ): Cursor? { + return context?.contentResolver?.query( + getUnderlyingUriFromUri(uri), + projection, + selection, + selectionArgs, + sortOrder + ) + } + + override fun getType(uri: Uri): String? { + return context?.contentResolver?.getType(getUnderlyingUriFromUri(uri)) + } + + override fun insert(uri: Uri, values: ContentValues?): Uri? { + return context?.contentResolver?.insert(getUnderlyingUriFromUri(uri), values) + } + + override fun update( + uri: Uri, + values: ContentValues?, + selection: String?, + selectionArgs: Array? + ): Int { + return context?.contentResolver?.update( + getUnderlyingUriFromUri(uri), + values, + selection, + selectionArgs + ) ?: 0 + } + + override fun delete( + uri: Uri, + selection: String?, + selectionArgs: Array? + ): Int { + return context?.contentResolver?.delete( + getUnderlyingUriFromUri(uri), + selection, + selectionArgs + ) ?: 0 + } + + @Throws(FileNotFoundException::class) + override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? { + return context?.contentResolver?.openFileDescriptor( + getUnderlyingUriFromUri(uri), + mode) + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/moez/QKSMS/model/MmsPart.kt b/domain/src/main/java/com/moez/QKSMS/model/MmsPart.kt index 0afb53b1..0e13988c 100644 --- a/domain/src/main/java/com/moez/QKSMS/model/MmsPart.kt +++ b/domain/src/main/java/com/moez/QKSMS/model/MmsPart.kt @@ -18,12 +18,17 @@ */ package dev.octoshrimpy.quik.model +import android.content.ContentResolver +import android.content.ContentResolver.MimeTypeInfo +import android.net.Uri +import android.webkit.MimeTypeMap import androidx.core.net.toUri import io.realm.RealmObject import io.realm.RealmResults import io.realm.annotations.Index import io.realm.annotations.LinkingObjects import io.realm.annotations.PrimaryKey +import java.io.File open class MmsPart : RealmObject() { @@ -37,13 +42,23 @@ open class MmsPart : RealmObject() { @LinkingObjects("parts") val messages: RealmResults? = null - fun getUri() = "content://mms/part/$id".toUri() + fun getUri(): Uri = Uri + .Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority("mms") + .encodedPath("part/$id") + .build() + + fun getBestFilename(): String = + if (File(name).extension.isNotEmpty()) name ?: "unknown" + else "$name." + (MimeTypeMap.getSingleton().getExtensionFromMimeType(type) + ?: type.substringAfter("/")) fun getSummary(): String? = when { type == "application/smil" -> null type == "text/plain" -> text type == "text/x-vcard" -> "Contact card" - type.startsWith("image") -> "Photo" + type.startsWith("image") -> "Picture" type.startsWith("video") -> "Video" type.startsWith("audio") -> "Audio" else -> type.substring(type.indexOf('/') + 1) diff --git a/presentation/src/main/AndroidManifest.xml b/presentation/src/main/AndroidManifest.xml index e2bddfa4..f93b74f3 100644 --- a/presentation/src/main/AndroidManifest.xml +++ b/presentation/src/main/AndroidManifest.xml @@ -252,17 +252,6 @@ - - - - - diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/AttachmentAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/AttachmentAdapter.kt index 3761d404..8d407b42 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/AttachmentAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/AttachmentAdapter.kt @@ -73,6 +73,8 @@ class AttachmentAdapter @Inject constructor( override fun onBindViewHolder(holder: QkViewHolder, position: Int) { val attachment = getItem(position) + holder.fileName.visibility = View.GONE + if (attachment.isVCard(context)) { val displayName = Ezvcard.parse( String(attachment.getResourceBytes(context)) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeViewModel.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeViewModel.kt index a94558bd..14242737 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeViewModel.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeViewModel.kt @@ -24,6 +24,7 @@ import android.os.Vibrator import android.provider.ContactsContract import android.telephony.SmsMessage import androidx.core.content.getSystemService +import com.moez.QKSMS.contentproviders.MmsPartProvider import dev.octoshrimpy.quik.R import dev.octoshrimpy.quik.common.Navigator import dev.octoshrimpy.quik.common.base.QkViewModel @@ -470,12 +471,11 @@ class ComposeViewModel @Inject constructor( .mapNotNull(messageRepo::getPart) .filter { part -> !part.isImage() && !part.isVideo() } .autoDisposable(view.scope()) - .subscribe { part -> - if (permissionManager.hasStorage()) { - messageRepo.savePart(part.id)?.let { navigator.viewFile(it, part.type) } - } else { - view.requestStoragePermission() - } + .subscribe { + navigator.viewFile( + MmsPartProvider.getUriForMmsPartId(it.id, it.getBestFilename()), + it.type + ) } // Update the State when the message selected count changes diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/AudioBinder.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/AudioBinder.kt index b253185c..89d2352d 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/AudioBinder.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/AudioBinder.kt @@ -25,7 +25,9 @@ import android.media.MediaMetadataRetriever import android.view.View import android.widget.SeekBar import com.moez.QKSMS.common.QkMediaPlayer +import com.moez.QKSMS.contentproviders.MmsPartProvider import dev.octoshrimpy.quik.R +import dev.octoshrimpy.quik.common.Navigator import dev.octoshrimpy.quik.common.base.QkViewHolder import dev.octoshrimpy.quik.common.util.Colors import dev.octoshrimpy.quik.common.util.extensions.resolveThemeColor @@ -49,6 +51,12 @@ import javax.inject.Inject class AudioBinder @Inject constructor(colors: Colors, private val context: Context) : PartBinder() { + companion object { + const val DEFAULT_SHARE_FILENAME = "quik-audio-attachment.mp3" + } + + @Inject lateinit var navigator: Navigator + override val partLayout = R.layout.mms_audio_preview_list_item override var theme = colors.theme() @@ -169,7 +177,13 @@ class AudioBinder @Inject constructor(colors: Colors, private val context: Conte } } - holder.containerView.setOnClickListener { clicks.onNext(part.id) } + // share button click handling + holder.share.setOnClickListener { + navigator.shareFile( + MmsPartProvider.getUriForMmsPartId(part.id, part.getBestFilename()), + part.type + ) + } // if this item is the active active audio item update the active view holder if (audioState.partId == part.id) @@ -224,6 +238,10 @@ class AudioBinder @Inject constructor(colors: Colors, private val context: Conte setBackgroundTint(primaryColor) } + // share button + holder.share.setTint(secondaryColor) + holder.share.setBackgroundTint(primaryColor) + MediaMetadataRetriever().use { it.setDataSource(context, part.getUri()) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/FileBinder.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/FileBinder.kt index 0bb60856..7fe60f4b 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/FileBinder.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/FileBinder.kt @@ -22,7 +22,9 @@ import android.annotation.SuppressLint import android.content.Context import android.view.Gravity import android.widget.FrameLayout +import com.moez.QKSMS.contentproviders.MmsPartProvider import dev.octoshrimpy.quik.R +import dev.octoshrimpy.quik.common.Navigator import dev.octoshrimpy.quik.common.base.QkViewHolder import dev.octoshrimpy.quik.common.util.Colors import dev.octoshrimpy.quik.common.util.extensions.resolveThemeColor @@ -34,11 +36,14 @@ import dev.octoshrimpy.quik.model.MmsPart import io.reactivex.Observable import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers +import kotlinx.android.synthetic.main.mms_audio_preview_list_item.share import kotlinx.android.synthetic.main.mms_file_list_item.* import javax.inject.Inject class FileBinder @Inject constructor(colors: Colors, private val context: Context) : PartBinder() { + @Inject lateinit var navigator: Navigator + override val partLayout = R.layout.mms_file_list_item override var theme = colors.theme() @@ -56,8 +61,6 @@ class FileBinder @Inject constructor(colors: Colors, private val context: Contex BubbleUtils.getBubble(false, canGroupWithPrevious, canGroupWithNext, message.isMe()) .let(holder.fileBackground::setBackgroundResource) - holder.containerView.setOnClickListener { clicks.onNext(part.id) } - Observable.just(part.getUri()) .map(context.contentResolver::openInputStream) .map { inputStream -> inputStream.use { it.available() } } @@ -73,6 +76,14 @@ class FileBinder @Inject constructor(colors: Colors, private val context: Contex .observeOn(AndroidSchedulers.mainThread()) .subscribe { size -> holder.size.text = size } + // share button click handling + holder.share.setOnClickListener { + navigator.shareFile( + MmsPartProvider.getUriForMmsPartId(part.id, part.getBestFilename()), + part.type + ) + } + holder.filename.text = part.name val params = holder.fileBackground.layoutParams as FrameLayout.LayoutParams @@ -82,12 +93,16 @@ class FileBinder @Inject constructor(colors: Colors, private val context: Contex holder.icon.setTint(theme.textPrimary) holder.filename.setTextColor(theme.textPrimary) holder.size.setTextColor(theme.textTertiary) + holder.share.setTint(theme.theme) + holder.share.setBackgroundTint(theme.textPrimary) } else { holder.fileBackground.layoutParams = params.apply { gravity = Gravity.END } holder.fileBackground.setBackgroundTint(holder.containerView.context.resolveThemeColor(R.attr.bubbleColor)) holder.icon.setTint(holder.containerView.context.resolveThemeColor(android.R.attr.textColorSecondary)) holder.filename.setTextColor(holder.containerView.context.resolveThemeColor(android.R.attr.textColorPrimary)) holder.size.setTextColor(holder.containerView.context.resolveThemeColor(android.R.attr.textColorTertiary)) + holder.share.setTint(holder.containerView.context.resolveThemeColor(R.attr.bubbleColor)) + holder.share.setBackgroundTint(holder.containerView.context.resolveThemeColor(android.R.attr.textColorTertiary)) } } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/gallery/GalleryViewModel.kt b/presentation/src/main/java/com/moez/QKSMS/feature/gallery/GalleryViewModel.kt index 43d4432e..6e4950d9 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/gallery/GalleryViewModel.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/gallery/GalleryViewModel.kt @@ -19,6 +19,7 @@ package dev.octoshrimpy.quik.feature.gallery import android.content.Context +import com.moez.QKSMS.contentproviders.MmsPartProvider import dev.octoshrimpy.quik.R import dev.octoshrimpy.quik.common.Navigator import dev.octoshrimpy.quik.common.base.QkViewModel @@ -45,6 +46,9 @@ class GalleryViewModel @Inject constructor( private val saveImage: SaveImage, private val permissions: PermissionManager ) : QkViewModel(GalleryState()) { + companion object { + const val DEFAULT_SHARE_FILENAME = "quik-media-attachment.jpg" + } init { disposables += Flowable.just(partId) @@ -80,11 +84,13 @@ class GalleryViewModel @Inject constructor( // Share image externally view.optionsItemSelected() .filter { itemId -> itemId == R.id.share } - .filter { permissions.hasStorage().also { if (!it) view.requestStoragePermission() } } .withLatestFrom(view.pageChanged()) { _, part -> part } .autoDisposable(view.scope()) - .subscribe { part -> - messageRepo.savePart(part.id)?.let { navigator.shareFile(it, part.type) } + .subscribe { + navigator.shareFile( + MmsPartProvider.getUriForMmsPartId(it.id, it.getBestFilename()), + it.type + ) } } diff --git a/presentation/src/main/res/layout/mms_audio_preview_list_item.xml b/presentation/src/main/res/layout/mms_audio_preview_list_item.xml index 97fc8839..a9f65b55 100644 --- a/presentation/src/main/res/layout/mms_audio_preview_list_item.xml +++ b/presentation/src/main/res/layout/mms_audio_preview_list_item.xml @@ -53,30 +53,29 @@ android:progressTint="@color/tools_theme" android:thumbTint="@color/tools_theme" /> - + android:layout_marginBottom="12dp"> + android:textColor="@color/black" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@id/share" + app:layout_constraintStart_toEndOf="@id/playPause" /> - + + \ No newline at end of file diff --git a/presentation/src/main/res/layout/mms_file_list_item.xml b/presentation/src/main/res/layout/mms_file_list_item.xml index d8ee901b..8acc1d77 100644 --- a/presentation/src/main/res/layout/mms_file_list_item.xml +++ b/presentation/src/main/res/layout/mms_file_list_item.xml @@ -35,36 +35,53 @@ android:layout_height="24dp" android:layout_marginStart="12dp" android:src="@drawable/ic_attachment_black_24dp" - android:tint="?android:attr/textColorSecondary" - app:layout_constraintBottom_toBottomOf="@id/fileBackground" + app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="@id/fileBackground" /> - - + app:tint="?android:attr/textColorSecondary" /> - + + + + + + + + diff --git a/presentation/src/main/res/xml/provider_paths.xml b/presentation/src/main/res/xml/provider_paths.xml deleted file mode 100644 index 98b55d83..00000000 --- a/presentation/src/main/res/xml/provider_paths.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - From 53a634135f0581c0a58c17c03f3a6ffa69235a71 Mon Sep 17 00:00:00 2001 From: gavine99 Date: Wed, 5 Feb 2025 13:28:39 +1100 Subject: [PATCH 2/2] fix crashing bug attaching contact card --- .../java/com/moez/QKSMS/feature/compose/AttachmentAdapter.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/AttachmentAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/AttachmentAdapter.kt index 8d407b42..8ec5cf4c 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/AttachmentAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/AttachmentAdapter.kt @@ -73,8 +73,6 @@ class AttachmentAdapter @Inject constructor( override fun onBindViewHolder(holder: QkViewHolder, position: Int) { val attachment = getItem(position) - holder.fileName.visibility = View.GONE - if (attachment.isVCard(context)) { val displayName = Ezvcard.parse( String(attachment.getResourceBytes(context)) @@ -84,6 +82,8 @@ class AttachmentAdapter @Inject constructor( return } + holder.fileName.visibility = View.GONE + // if attachment uri is missing if (!attachment.getUri().resourceExists(context)) { holder.thumbnail.setImageResource(android.R.drawable.ic_delete)