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 682e7415..d8a7d70b 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 @@ -90,6 +90,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) 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 @@ - - - - -