Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for video and audio attachments #1941

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ public class ContentType {
public static final String AUDIO_3GPP = "audio/3gpp";
public static final String AUDIO_X_WAV = "audio/x-wav";
public static final String AUDIO_OGG = "application/ogg";
public static final String AUDIO_OGG_ALT = "audio/ogg";

public static final String VIDEO_UNSPECIFIED = "video/*";
public static final String VIDEO_3GPP = "video/3gpp";
Expand Down Expand Up @@ -121,6 +122,7 @@ public class ContentType {
sSupportedContentTypes.add(AUDIO_X_WAV);
sSupportedContentTypes.add(AUDIO_3GPP);
sSupportedContentTypes.add(AUDIO_OGG);
sSupportedContentTypes.add(AUDIO_OGG_ALT);

sSupportedContentTypes.add(VIDEO_3GPP);
sSupportedContentTypes.add(VIDEO_3G2);
Expand Down Expand Up @@ -165,6 +167,7 @@ public class ContentType {
sSupportedAudioTypes.add(AUDIO_X_WAV);
sSupportedAudioTypes.add(AUDIO_3GPP);
sSupportedAudioTypes.add(AUDIO_OGG);
sSupportedAudioTypes.add(AUDIO_OGG_ALT);

// add supported video types
sSupportedVideoTypes.add(VIDEO_3GPP);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ class MessageRepositoryImpl @Inject constructor(
.contains("type", "image/")
.or()
.contains("type", "video/")
.or()
.contains("type", "audio/")
.endGroup()
.sort("id", Sort.DESCENDING)
.findAllAsync()
Expand Down Expand Up @@ -346,14 +348,20 @@ class MessageRepositoryImpl @Inject constructor(
val maxHeight = smsManager.carrierConfigValues.getInt(SmsManager.MMS_CONFIG_MAX_IMAGE_HEIGHT)
.takeIf { prefs.mmsSize.get() == -1 } ?: Int.MAX_VALUE

var remainingBytes = when (prefs.mmsSize.get()) {
-1 -> smsManager.carrierConfigValues.getInt(SmsManager.MMS_CONFIG_MAX_MESSAGE_SIZE)
val carrierLimit = smsManager.carrierConfigValues.getInt(SmsManager.MMS_CONFIG_MAX_MESSAGE_SIZE)

val compressionLimit = when (prefs.mmsSize.get()) {
-1 -> carrierLimit
0 -> Int.MAX_VALUE
else -> prefs.mmsSize.get() * 1024
} * 0.9 // Ugly, but buys us a bit of wiggle room

var bytesUsed = 0.0

var hasNotSkippedAttachment = true

signedBody.takeIf { it.isNotEmpty() }?.toByteArray()?.let { bytes ->
remainingBytes -= bytes.size
bytesUsed += bytes.size
parts += MMSPart("text", ContentType.TEXT_PLAIN, bytes)
}

Expand All @@ -362,10 +370,53 @@ class MessageRepositoryImpl @Inject constructor(
.mapNotNull { attachment -> attachment as? Attachment.Contact }
.map { attachment -> attachment.vCard.toByteArray() }
.map { vCard ->
remainingBytes -= vCard.size
bytesUsed += vCard.size
MMSPart("contact", ContentType.TEXT_VCARD, vCard)
}

val videoBytesByAttachment = attachments
.mapNotNull { attachment -> attachment as? Attachment.Video }
.associateWith { attachment ->
var uri = attachment.getUri() ?: return@associateWith byteArrayOf()
val inputStream = context.contentResolver.openInputStream(uri) ?: return@associateWith byteArrayOf()
inputStream.readBytes()
}
.toMutableMap()

videoBytesByAttachment.forEach { (attachment, bytes) ->
val contentType: String = attachment.getContentType(context) ?: return@forEach
if (bytes.size < carrierLimit - bytesUsed) {
parts += MMSPart("video", contentType, bytes)
bytesUsed += bytes.size
} else {
Timber.w("Video file of ${bytes.size / 1024}Kb cannot be sent in ${(carrierLimit - bytesUsed).toInt() / 1024}Kb remaining space.")
hasNotSkippedAttachment = false
}
}

val fileBytesByAttachment = attachments
.mapNotNull { attachment -> attachment as? Attachment.File }
.associateWith { attachment ->
val uri = attachment.getUri() ?: return@associateWith byteArrayOf()
val inputStream = context.contentResolver.openInputStream(uri) ?: return@associateWith byteArrayOf()
inputStream.readBytes()
}
.toMutableMap()

fileBytesByAttachment.forEach { (attachment, bytes) ->
val contentType: String = attachment.getContentType(context) ?: return@forEach
if (contentType.let(ContentType::isSupportedAudioType)) {
if (bytes.size < carrierLimit - bytesUsed) {
parts += MMSPart(attachment.getName(context)
?: "Attachment", contentType, bytes)
bytesUsed += bytes.size
} else {
Timber.w("File of ${bytes.size / 1024}Kb cannot be sent in ${(carrierLimit - bytesUsed).toInt() / 1024}Kb remaining space.")
hasNotSkippedAttachment = false
}
}
}

val imageBytesByAttachment = attachments
.mapNotNull { attachment -> attachment as? Attachment.Image }
.associateWith { attachment ->
Expand All @@ -378,10 +429,10 @@ class MessageRepositoryImpl @Inject constructor(
.toMutableMap()

val imageByteCount = imageBytesByAttachment.values.sumBy { byteArray -> byteArray.size }
if (imageByteCount > remainingBytes) {
if (imageByteCount > compressionLimit - bytesUsed) {
imageBytesByAttachment.forEach { (attachment, originalBytes) ->
val uri = attachment.getUri() ?: return@forEach
val maxBytes = originalBytes.size / imageByteCount.toFloat() * remainingBytes
val maxBytes = originalBytes.size / imageByteCount.toFloat() * (compressionLimit - bytesUsed)

// Get the image dimensions
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
Expand Down Expand Up @@ -430,7 +481,10 @@ class MessageRepositoryImpl @Inject constructor(
// We need to strip the separators from outgoing MMS, or else they'll appear to have sent and not go through
val transaction = Transaction(context)
val recipients = addresses.map(phoneNumberUtils::normalizeNumber)
transaction.sendNewMessage(subId, threadId, recipients, parts, null, null)

if (parts.isNotEmpty() && hasNotSkippedAttachment) {
transaction.sendNewMessage(subId, threadId, recipients, parts, null, null)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,18 @@ class SendScheduledMessage @Inject constructor(
}
.map { message ->
val threadId = TelephonyCompat.getOrCreateThreadId(context, message.recipients)
val attachments = message.attachments.mapNotNull(Uri::parse).map { Attachment.Image(it) }
val attachments = message.attachments.mapNotNull(Uri::parse).mapNotNull { uri ->
val contentType: String? = uri.let(context.contentResolver::getType)
if (contentType?.startsWith("image/") == true) {
Attachment.Image(uri)
} else if (contentType?.startsWith("video") == true) {
Attachment.Video(uri)
} else if (contentType != null) {
Attachment.File(uri)
} else {
null
}
}
SendMessage.Params(message.subId, threadId, message.recipients, message.body, attachments)
}
.flatMap(sendMessage::buildObservable)
Expand Down
62 changes: 60 additions & 2 deletions domain/src/main/java/com/moez/QKSMS/model/Attachment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ package com.moez.QKSMS.model
import android.content.Context
import android.net.Uri
import android.os.Build
import android.provider.OpenableColumns
import androidx.core.view.inputmethod.InputContentInfoCompat

sealed class Attachment {

data class Image(
private val uri: Uri? = null,
private val inputContent: InputContentInfoCompat? = null
Expand All @@ -47,8 +47,66 @@ sealed class Attachment {
}
}

data class Contact(val vCard: String) : Attachment()
data class Video(
private val uri: Uri? = null,
private val inputContent: InputContentInfoCompat? = null
) : Attachment() {
fun getUri(): Uri? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
inputContent?.contentUri ?: uri
} else {
uri
}
}

fun getContentType(context: Context): String? {
return getUri()?.let(context.contentResolver::getType)
}
}

data class File(
private val uri: Uri? = null,
private val inputContent: InputContentInfoCompat? = null
) : Attachment() {

fun getUri(): Uri? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
inputContent?.contentUri ?: uri
} else {
uri
}
}

fun getContentType(context: Context): String? {
return getUri()?.let(context.contentResolver::getType)
}

fun getName(context: Context): String? {
return getUri()?.let {
// The selection parameter can be null since the intent only opens one file.
returnUri ->
context.contentResolver.query(returnUri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null)
}?.use { cursor ->
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
cursor.moveToFirst()
return cursor.getString(nameIndex)
}
}

fun getSize(context: Context): Long? {
return getUri()?.let {
// The selection parameter can be null since the intent only opens one file.
returnUri ->
context.contentResolver.query(returnUri, arrayOf(OpenableColumns.SIZE), null, null, null)
}?.use { cursor ->
val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
cursor.moveToFirst()
return cursor.getLong(sizeIndex)
}
}
}

data class Contact(val vCard: String) : Attachment()
}

class Attachments(attachments: List<Attachment>) : List<Attachment> by attachments
1 change: 1 addition & 0 deletions domain/src/main/java/com/moez/QKSMS/model/MmsPart.kt
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ open class MmsPart : RealmObject() {
type == "text/x-vCard" -> "Contact card"
type.startsWith("image") -> "Photo"
type.startsWith("video") -> "Video"
type.startsWith("audio") -> "Audio"
else -> null
}

Expand Down
20 changes: 20 additions & 0 deletions presentation/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,26 @@
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/x-vcard" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="video/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="video/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="audio/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="audio/*" />
</intent-filter>

<meta-data
android:name="android.service.chooser.chooser_target_service"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,15 @@ import com.moez.QKSMS.common.base.QkViewHolder
import com.moez.QKSMS.common.util.extensions.getDisplayName
import com.moez.QKSMS.extensions.mapNotNull
import com.moez.QKSMS.model.Attachment
import com.moez.QKSMS.util.GlideApp
import ezvcard.Ezvcard
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import io.reactivex.subjects.PublishSubject
import io.reactivex.subjects.Subject
import kotlinx.android.synthetic.main.attachment_contact_list_item.*
import kotlinx.android.synthetic.main.attachment_file_list_item.*
import kotlinx.android.synthetic.main.attachment_image_list_item.*
import kotlinx.android.synthetic.main.attachment_image_list_item.view.*
import javax.inject.Inject
Expand All @@ -47,6 +49,9 @@ class AttachmentAdapter @Inject constructor(
companion object {
private const val VIEW_TYPE_IMAGE = 0
private const val VIEW_TYPE_CONTACT = 1
private const val VIEW_TYPE_VIDEO = 2
private const val VIEW_TYPE_FILE = 3

}

val attachmentDeleted: Subject<Attachment> = PublishSubject.create()
Expand All @@ -59,6 +64,11 @@ class AttachmentAdapter @Inject constructor(

VIEW_TYPE_CONTACT -> inflater.inflate(R.layout.attachment_contact_list_item, parent, false)

VIEW_TYPE_VIDEO -> inflater.inflate(R.layout.attachment_image_list_item, parent, false)
.apply { thumbnailBounds.clipToOutline = true }

VIEW_TYPE_FILE -> inflater.inflate(R.layout.attachment_file_list_item, parent, false)

else -> null!! // Impossible
}

Expand Down Expand Up @@ -87,12 +97,41 @@ class AttachmentAdapter @Inject constructor(
holder.name?.text = displayName
holder.name?.isVisible = displayName.isNotEmpty()
}
is Attachment.Video -> {
GlideApp.with(context).load(attachment.getUri()).fitCenter().into(holder.thumbnail)
}
is Attachment.File -> {
Observable.just(attachment.getName(context))
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { name ->
holder.filename?.text = name
holder.filename?.isVisible = name?.isNotEmpty() ?: false
}
Observable.just(attachment.getSize(context))
.map { bytes ->
when (bytes) {
in 0..999 -> "$bytes B"
in 1000..999999 -> "${"%.1f".format(bytes / 1000f)} KB"
in 1000000..9999999 -> "${"%.1f".format(bytes / 1000000f)} MB"
else -> "${"%.1f".format(bytes / 1000000000f)} GB"
}
}
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { size ->
holder.size?.text = size
holder.size?.isVisible = size?.isNotEmpty() ?: false
}
}
}
}

override fun getItemViewType(position: Int) = when (getItem(position)) {
is Attachment.Image -> VIEW_TYPE_IMAGE
is Attachment.Contact -> VIEW_TYPE_CONTACT
is Attachment.Video -> VIEW_TYPE_VIDEO
is Attachment.File -> VIEW_TYPE_FILE
}

}
Loading