diff --git a/android-smsmms/src/main/java/com/android/mms/transaction/RetryScheduler.java b/android-smsmms/src/main/java/com/android/mms/transaction/RetryScheduler.java
index 414f14e7..a107f3ba 100755
--- a/android-smsmms/src/main/java/com/android/mms/transaction/RetryScheduler.java
+++ b/android-smsmms/src/main/java/com/android/mms/transaction/RetryScheduler.java
@@ -143,6 +143,11 @@ public class RetryScheduler implements Observer {
case PduHeaders.RESPONSE_STATUS_ERROR_PERMANENT_MESSAGE_NOT_FOUND:
errorString = R.string.service_message_not_found;
break;
+ case PduHeaders.RESPONSE_STATUS_ERROR_CONTENT_NOT_ACCEPTED:
+ case PduHeaders.RESPONSE_STATUS_ERROR_PERMANENT_CONTENT_NOT_ACCEPTED:
+ errorString = R.string.service_message_content_not_accepted;
+ break;
+
}
if (errorString != 0) {
DownloadManager.init(mContext.getApplicationContext());
diff --git a/android-smsmms/src/main/res/values/strings.xml b/android-smsmms/src/main/res/values/strings.xml
index c48bc4b8..edcfd758 100755
--- a/android-smsmms/src/main/res/values/strings.xml
+++ b/android-smsmms/src/main/res/values/strings.xml
@@ -20,6 +20,7 @@ limitations under the License.
Invalid destination address.
Service not activated on network.
Message expired or not available.
+ Message content is not accepted.
Couldn\'t send due to network problem.
Currently can\'t send your message. It will be sent when the service becomes available.
Can\'t download right now. Try again later.
diff --git a/data/src/main/AndroidManifest.xml b/data/src/main/AndroidManifest.xml
index b1ca12fe..a65cc645 100644
--- a/data/src/main/AndroidManifest.xml
+++ b/data/src/main/AndroidManifest.xml
@@ -19,4 +19,5 @@
+
diff --git a/data/src/main/java/com/moez/QKSMS/extensions/UriExtensions.kt b/data/src/main/java/com/moez/QKSMS/extensions/UriExtensions.kt
new file mode 100644
index 00000000..7ebe330a
--- /dev/null
+++ b/data/src/main/java/com/moez/QKSMS/extensions/UriExtensions.kt
@@ -0,0 +1,36 @@
+/*
+ * 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 dev.octoshrimpy.quik.extensions
+
+import android.content.Context
+import android.net.Uri
+
+fun Uri.resourceExists(context: Context): Boolean {
+ // check if resource at uri still exists
+ try {
+ return context.contentResolver.query(
+ this, null, null, null, null
+ )?.use {
+ it.moveToFirst()
+ } ?: false
+ }
+ catch (e: Exception) { /* nothing */ }
+
+ return false
+}
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 e776c705..c4741436 100644
--- a/data/src/main/java/com/moez/QKSMS/repository/MessageRepositoryImpl.kt
+++ b/data/src/main/java/com/moez/QKSMS/repository/MessageRepositoryImpl.kt
@@ -25,7 +25,6 @@ import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
-import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Build
import android.os.Environment
@@ -65,14 +64,7 @@ import io.realm.Case
import io.realm.Realm
import io.realm.RealmResults
import io.realm.Sort
-import okio.buffer
-import okio.source
import timber.log.Timber
-import java.io.File
-import java.io.FileNotFoundException
-import java.io.FileOutputStream
-import java.io.IOException
-import java.util.*
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Singleton
@@ -376,27 +368,37 @@ class MessageRepositoryImpl @Inject constructor(
parts += MMSPart("text", ContentType.TEXT_PLAIN, bytes)
}
- // Attach contacts
+ // Attach those that can't be compressed (ie. everything but not images)
parts += attachments
- .mapNotNull { attachment -> attachment as? Attachment.Contact }
- .map { attachment -> attachment.vCard.toByteArray() }
- .map { vCard ->
- remainingBytes -= vCard.size
- MMSPart("contact", ContentType.TEXT_VCARD, vCard)
- }
+ // filter out images
+ .filter { !it.isImage(context) }
+ // filter out items that have no data available (ie user may have deleted
+ // the file from storage)
+ .filter { it.getResourceBytes(context).isNotEmpty() }
+ .map {
+ remainingBytes -= it.getResourceBytes(context).size
+ MMSPart(
+ it.getName(context),
+ it.getType(context),
+ it.getResourceBytes(context)
+ )
+ }
val imageBytesByAttachment = attachments
- .mapNotNull { attachment -> attachment as? Attachment.Image }
- .associateWith { attachment ->
- val uri = attachment.getUri() ?: return@associateWith byteArrayOf()
- when (attachment.isGif(context)) {
- true -> ImageUtils.getScaledGif(context, uri, maxWidth, maxHeight)
- false -> ImageUtils.getScaledImage(context, uri, maxWidth, maxHeight)
- }
+ // filter out non-images
+ .filter { it.isImage(context) }
+ // filter out items that have no data available (ie user may have deleted
+ // the file from storage)
+ .filter { it.getResourceBytes(context).isNotEmpty() }
+ .associateWith {
+ when (it.getType(context) == "image/gif") {
+ true -> ImageUtils.getScaledGif(context, it.getUri(), maxWidth, maxHeight)
+ false -> ImageUtils.getScaledImage(context, it.getUri(), maxWidth, maxHeight)
}
- .toMutableMap()
+ }
+ .toMutableMap()
- val imageByteCount = imageBytesByAttachment.values.sumBy { byteArray -> byteArray.size }
+ val imageByteCount = imageBytesByAttachment.values.sumOf { it.size }
if (imageByteCount > remainingBytes) {
imageBytesByAttachment.forEach { (attachment, originalBytes) ->
val uri = attachment.getUri() ?: return@forEach
@@ -426,9 +428,9 @@ class MessageRepositoryImpl @Inject constructor(
val newHeight = (newWidth / aspectRatio).toInt()
attempts++
- scaledBytes = when (attachment.isGif(context)) {
- true -> ImageUtils.getScaledGif(context, uri, newWidth, newHeight, 80)
- false -> ImageUtils.getScaledImage(context, uri, newWidth, newHeight, 80)
+ scaledBytes = when (attachment.getType(context) == "image/gif") {
+ true -> ImageUtils.getScaledGif(context, attachment.getUri(), maxWidth, maxHeight)
+ false -> ImageUtils.getScaledImage(context, attachment.getUri(), maxWidth, maxHeight)
}
Timber.d("Compression attempt $attempts: ${scaledBytes.size / 1024}/${maxBytes.toInt() / 1024}Kb ($width*$height -> $newWidth*$newHeight)")
@@ -440,7 +442,7 @@ class MessageRepositoryImpl @Inject constructor(
}
imageBytesByAttachment.forEach { (attachment, bytes) ->
- parts += when (attachment.isGif(context)) {
+ parts += when (attachment.getType(context) == "image/gif") {
true -> MMSPart("image", ContentType.IMAGE_GIF, bytes)
false -> MMSPart("image", ContentType.IMAGE_JPEG, bytes)
}
diff --git a/domain/src/main/java/com/moez/QKSMS/interactor/SendScheduledMessage.kt b/domain/src/main/java/com/moez/QKSMS/interactor/SendScheduledMessage.kt
index 8b76318b..0047fdd8 100644
--- a/domain/src/main/java/com/moez/QKSMS/interactor/SendScheduledMessage.kt
+++ b/domain/src/main/java/com/moez/QKSMS/interactor/SendScheduledMessage.kt
@@ -47,7 +47,7 @@ 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).map { Attachment(it) }
SendMessage.Params(message.subId, threadId, message.recipients, message.body, attachments)
}
.flatMap(sendMessage::buildObservable)
diff --git a/domain/src/main/java/com/moez/QKSMS/model/Attachment.kt b/domain/src/main/java/com/moez/QKSMS/model/Attachment.kt
index c354f60f..bb25625e 100644
--- a/domain/src/main/java/com/moez/QKSMS/model/Attachment.kt
+++ b/domain/src/main/java/com/moez/QKSMS/model/Attachment.kt
@@ -21,33 +21,106 @@ package dev.octoshrimpy.quik.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
- ) : Attachment() {
+class Attachment(
+ private val uri: Uri? = null,
+ private val inputContent: InputContentInfoCompat? = null
+) {
+ private var resourceBytes: ByteArray? = null
- fun getUri(): Uri? {
- return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
- inputContent?.contentUri ?: uri
- } else {
- uri
- }
- }
-
- fun isGif(context: Context): Boolean {
- return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1 && inputContent != null) {
- inputContent.description.hasMimeType("image/gif")
- } else {
- uri?.let(context.contentResolver::getType) == "image/gif"
- }
- }
+ fun getUri(): Uri {
+ return (
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1)
+ inputContent?.contentUri ?: uri ?: Uri.EMPTY
+ else uri ?: Uri.EMPTY
+ )
}
- data class Contact(val vCard: String) : Attachment()
+ fun getType(context: Context): String {
+ return context.contentResolver.getType(getUri()) ?: "application/octect-stream"
+ }
+
+ fun getName(context: Context): String {
+ try {
+ context.contentResolver.query(
+ getUri(),
+ arrayOf(OpenableColumns.DISPLAY_NAME),
+ null,
+ null,
+ null
+ )
+ ?.use { cursor ->
+ val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
+ if (nameIndex == -1)
+ return "unknown"
+ cursor.moveToFirst()
+ return cursor.getString(nameIndex) ?: "unknown"
+ }
+ }
+ catch (e: Exception) { /* nothing*/ }
+
+ return "unknown"
+ }
+
+ fun isVCard(context: Context): Boolean {
+ return (getType(context) == "text/x-vcard")
+ }
+
+ fun hasDisplayableImage(context: Context): Boolean {
+ val mimeType = getType(context)
+ return (mimeType.startsWith("image/") || mimeType.startsWith("video/"))
+ }
+
+ fun isAudio(context: Context): Boolean {
+ val mimeType = getType(context)
+ return (mimeType.startsWith("audio/"))
+ }
+
+ fun isImage(context: Context): Boolean {
+ val mimeType = getType(context)
+ return (mimeType.startsWith("image/"))
+ }
+
+ fun getSize(context: Context): Long {
+ try {
+ context.contentResolver.query(
+ getUri(),
+ arrayOf(OpenableColumns.SIZE),
+ null,
+ null,
+ null
+ )
+ ?.use {
+ val sizeIndex = it.getColumnIndex(OpenableColumns.SIZE)
+ if (sizeIndex == -1)
+ return -1
+ it.moveToFirst()
+ return it.getLong(sizeIndex)
+ }
+ }
+ catch (e: Exception) { /* nothing */ }
+
+ return -1
+ }
+
+ fun getResourceBytes(context: Context): ByteArray {
+ if (resourceBytes != null)
+ return resourceBytes!!
+
+ resourceBytes = ByteArray(0)
+
+ try {
+ context.contentResolver.openInputStream(getUri())?.use {
+ resourceBytes = it.readBytes()
+ }
+ } catch (e: Exception) {
+ }
+
+ return resourceBytes!!
+ }
}
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 f9b62eb4..0afb53b1 100644
--- a/domain/src/main/java/com/moez/QKSMS/model/MmsPart.kt
+++ b/domain/src/main/java/com/moez/QKSMS/model/MmsPart.kt
@@ -40,11 +40,13 @@ open class MmsPart : RealmObject() {
fun getUri() = "content://mms/part/$id".toUri()
fun getSummary(): String? = when {
+ type == "application/smil" -> null
type == "text/plain" -> text
- type == "text/x-vCard" -> "Contact card"
+ type == "text/x-vcard" -> "Contact card"
type.startsWith("image") -> "Photo"
type.startsWith("video") -> "Video"
- else -> null
+ type.startsWith("audio") -> "Audio"
+ else -> type.substring(type.indexOf('/') + 1)
}
}
\ No newline at end of file
diff --git a/presentation/src/main/AndroidManifest.xml b/presentation/src/main/AndroidManifest.xml
index 4153b2c3..7b1c55ab 100644
--- a/presentation/src/main/AndroidManifest.xml
+++ b/presentation/src/main/AndroidManifest.xml
@@ -88,22 +88,12 @@
-
-
-
-
-
-
+
-
-
-
-
-
-
+
() {
companion object {
- private const val VIEW_TYPE_IMAGE = 0
+ private const val VIEW_TYPE_FILE = 0
private const val VIEW_TYPE_CONTACT = 1
}
@@ -53,14 +54,13 @@ class AttachmentAdapter @Inject constructor(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QkViewHolder {
val inflater = LayoutInflater.from(parent.context)
- val view = when (viewType) {
- VIEW_TYPE_IMAGE -> inflater.inflate(R.layout.attachment_image_list_item, parent, false)
- .apply { thumbnailBounds.clipToOutline = true }
- VIEW_TYPE_CONTACT -> inflater.inflate(R.layout.attachment_contact_list_item, parent, false)
-
- else -> null!! // Impossible
- }
+ val view = inflater.inflate(
+ if (viewType == VIEW_TYPE_CONTACT) R.layout.attachment_contact_list_item
+ else R.layout.attachment_file_list_item,
+ parent,
+ false
+ )
return QkViewHolder(view).apply {
view.setOnClickListener {
@@ -73,26 +73,81 @@ class AttachmentAdapter @Inject constructor(
override fun onBindViewHolder(holder: QkViewHolder, position: Int) {
val attachment = getItem(position)
- when (attachment) {
- is Attachment.Image -> Glide.with(context)
- .load(attachment.getUri())
- .into(holder.thumbnail)
-
- is Attachment.Contact -> Observable.just(attachment.vCard)
- .mapNotNull { vCard -> Ezvcard.parse(vCard).first() }
- .map { vcard -> vcard.getDisplayName() ?: "" }
- .subscribeOn(Schedulers.computation())
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe { displayName ->
- holder.name?.text = displayName
- holder.name?.isVisible = displayName.isNotEmpty()
- }
+ if (attachment.isVCard(context)) {
+ val displayName = Ezvcard.parse(
+ String(attachment.getResourceBytes(context))
+ ).first().getDisplayName() ?: ""
+ holder.name.text = displayName
+ holder.name.isVisible = displayName.isNotEmpty()
+ return
}
+
+ // if attachment uri is missing
+ if (!attachment.getUri().resourceExists(context)) {
+ holder.thumbnail.setImageResource(android.R.drawable.ic_delete)
+ holder.fileName.text = context.getString(R.string.attachment_missing)
+ holder.fileName.visibility = View.VISIBLE
+ return
+ }
+
+ val uri = attachment.getUri()
+ val mimeType = attachment.getType(context)
+
+ // if attachment mime type is image/* or video/*, use image/frame
+ if (attachment.hasDisplayableImage(context)) {
+ Glide
+ .with(context)
+ .load(uri)
+ .into(holder.thumbnail)
+ return
+ }
+
+ // if audio mime type, try and use embedded image if one exists
+ if (attachment.isAudio(context)) {
+ val metaDataRetriever = MediaMetadataRetriever()
+ metaDataRetriever.setDataSource(context, uri)
+ val embeddedPicture = metaDataRetriever.embeddedPicture
+ if (embeddedPicture != null) {
+ Glide
+ .with(context)
+ .load(embeddedPicture)
+ .into(holder.thumbnail)
+ return
+ }
+ }
+
+ // try and use icon from default app for type
+ val intent = Intent(Intent.ACTION_VIEW).setDataAndType(uri, mimeType)
+ val appIcon = context
+ .packageManager
+ .resolveActivity(intent, MATCH_DEFAULT)
+ ?.loadIcon(context.packageManager)
+ if (appIcon != null)
+ Glide
+ .with(context)
+ .load(appIcon)
+ .into(holder.thumbnail)
+ else if (mimeType?.startsWith("audio/") == true)
+ // else, if audio, use default local audio icon
+ Glide
+ .with(context)
+ .load(R.drawable.ic_round_volume_up_24)
+ .into(holder.thumbnail)
+ else
+ // else, use default attachment icon
+ Glide
+ .with(context)
+ .load(R.drawable.ic_attachment_black_24dp)
+ .into(holder.thumbnail)
+
+ // else, show file name
+ holder.fileName.text = attachment.getName(context)
+ holder.fileName.visibility = View.VISIBLE
}
- override fun getItemViewType(position: Int) = when (getItem(position)) {
- is Attachment.Image -> VIEW_TYPE_IMAGE
- is Attachment.Contact -> VIEW_TYPE_CONTACT
+ override fun getItemViewType(position: Int) = when (getItem(position).isVCard(context)) {
+ true -> VIEW_TYPE_CONTACT
+ else -> VIEW_TYPE_FILE
}
}
\ No newline at end of file
diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeActivity.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeActivity.kt
index ace8d547..c76ad421 100644
--- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeActivity.kt
+++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeActivity.kt
@@ -82,15 +82,6 @@ import javax.inject.Inject
class ComposeActivity : QkThemedActivity(), ComposeView {
- companion object {
- private const val SelectContactRequestCode = 0
- private const val TakePhotoRequestCode = 1
- private const val AttachPhotoRequestCode = 2
- private const val AttachContactRequestCode = 3
-
- private const val CameraDestinationKey = "camera_destination"
- }
-
@Inject lateinit var attachmentAdapter: AttachmentAdapter
@Inject lateinit var chipsAdapter: ChipsAdapter
@Inject lateinit var dateFormatter: DateFormatter
@@ -112,10 +103,11 @@ class ComposeActivity : QkThemedActivity(), ComposeView {
override val textChangedIntent by lazy { message.textChanges() }
override val attachIntent by lazy { Observable.merge(attach.clicks(), attachingBackground.clicks()) }
override val cameraIntent by lazy { Observable.merge(camera.clicks(), cameraLabel.clicks()) }
- override val galleryIntent by lazy { Observable.merge(gallery.clicks(), galleryLabel.clicks()) }
+ override val attachImageFileIntent by lazy { Observable.merge(gallery.clicks(), galleryLabel.clicks()) }
+ override val attachAnyFileIntent by lazy { Observable.merge(attachAFileIcon.clicks(), attachAFileLabel.clicks()) }
override val scheduleIntent by lazy { Observable.merge(schedule.clicks(), scheduleLabel.clicks()) }
override val attachContactIntent by lazy { Observable.merge(contact.clicks(), contactLabel.clicks()) }
- override val attachmentSelectedIntent: Subject = PublishSubject.create()
+ override val attachAnyFileSelectedIntent: Subject = PublishSubject.create()
override val contactSelectedIntent: Subject = PublishSubject.create()
override val inputContentIntent by lazy { message.inputContentSelected }
override val scheduleSelectedIntent: Subject = PublishSubject.create()
@@ -386,7 +378,7 @@ class ComposeActivity : QkThemedActivity(), ComposeView {
val intent = Intent(Intent.ACTION_PICK)
.setType(ContactsContract.CommonDataKinds.Phone.CONTENT_TYPE)
- startActivityForResult(Intent.createChooser(intent, null), AttachContactRequestCode)
+ startActivityForResult(Intent.createChooser(intent, null), ComposeView.AttachContactRequestCode)
}
override fun showContacts(sharing: Boolean, chips: List) {
@@ -395,7 +387,7 @@ class ComposeActivity : QkThemedActivity(), ComposeView {
val intent = Intent(this, ContactsActivity::class.java)
.putExtra(ContactsActivity.SharingKey, sharing)
.putExtra(ContactsActivity.ChipsKey, serialized)
- startActivityForResult(intent, SelectContactRequestCode)
+ startActivityForResult(intent, ComposeView.SelectContactRequestCode)
}
override fun themeChanged() {
@@ -415,17 +407,17 @@ class ComposeActivity : QkThemedActivity(), ComposeView {
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
.putExtra(MediaStore.EXTRA_OUTPUT, cameraDestination)
- startActivityForResult(Intent.createChooser(intent, null), TakePhotoRequestCode)
+ startActivityForResult(Intent.createChooser(intent, null), ComposeView.TakePhotoRequestCode)
}
- override fun requestGallery() {
+ override fun requestGallery(mimeType: String, requestCode: Int) {
val intent = Intent(Intent.ACTION_PICK)
.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
.putExtra(Intent.EXTRA_LOCAL_ONLY, false)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
- .setType("image/*")
- startActivityForResult(Intent.createChooser(intent, null), AttachPhotoRequestCode)
+ .setType(mimeType)
+ startActivityForResult(Intent.createChooser(intent, null), requestCode)
}
override fun setDraft(draft: String) {
@@ -473,36 +465,43 @@ class ComposeActivity : QkThemedActivity(), ComposeView {
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
- when {
- requestCode == SelectContactRequestCode -> {
+ if (resultCode != Activity.RESULT_OK)
+ return
+
+ when (requestCode) {
+ ComposeView.SelectContactRequestCode -> {
chipsSelectedIntent.onNext(data?.getSerializableExtra(ContactsActivity.ChipsKey)
?.let { serializable -> serializable as? HashMap }
?: hashMapOf())
}
- requestCode == TakePhotoRequestCode && resultCode == Activity.RESULT_OK -> {
- cameraDestination?.let(attachmentSelectedIntent::onNext)
+
+ ComposeView.TakePhotoRequestCode -> {
+ cameraDestination?.let(attachAnyFileSelectedIntent::onNext)
}
- requestCode == AttachPhotoRequestCode && resultCode == Activity.RESULT_OK -> {
+
+ ComposeView.AttachAFileRequestCode -> {
data?.clipData?.itemCount
- ?.let { count -> 0 until count }
- ?.mapNotNull { i -> data.clipData?.getItemAt(i)?.uri }
- ?.forEach(attachmentSelectedIntent::onNext)
- ?: data?.data?.let(attachmentSelectedIntent::onNext)
+ ?.let { count -> 0 until count }
+ ?.mapNotNull { i -> data.clipData?.getItemAt(i)?.uri }
+ ?.forEach(attachAnyFileSelectedIntent::onNext)
+ ?: data?.data?.let(attachAnyFileSelectedIntent::onNext)
}
- requestCode == AttachContactRequestCode && resultCode == Activity.RESULT_OK -> {
+
+ ComposeView.AttachContactRequestCode -> {
data?.data?.let(contactSelectedIntent::onNext)
}
+
else -> super.onActivityResult(requestCode, resultCode, data)
}
}
override fun onSaveInstanceState(outState: Bundle) {
- outState.putParcelable(CameraDestinationKey, cameraDestination)
+ outState.putParcelable(ComposeView.CameraDestinationKey, cameraDestination)
super.onSaveInstanceState(outState)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
- cameraDestination = savedInstanceState.getParcelable(CameraDestinationKey)
+ cameraDestination = savedInstanceState.getParcelable(ComposeView.CameraDestinationKey)
super.onRestoreInstanceState(savedInstanceState)
}
diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeActivityModule.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeActivityModule.kt
index 209680be..8580cb49 100644
--- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeActivityModule.kt
+++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeActivityModule.kt
@@ -78,22 +78,8 @@ class ComposeActivityModule {
val uris = mutableListOf()
activity.intent.getParcelableExtra(Intent.EXTRA_STREAM)?.run(uris::add)
activity.intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM)?.run(uris::addAll)
- return Attachments(uris.mapNotNull { uri ->
- val mimeType = activity.contentResolver.getType(uri)
- when {
- ContentType.isImageType(mimeType) -> {
- Attachment.Image(uri)
- }
- ContentType.TEXT_VCARD.equals(mimeType, true) -> {
- val inputStream = activity.contentResolver.openInputStream(uri)
- val text = inputStream?.reader(Charset.forName("utf-8"))?.readText()
- text?.let(Attachment::Contact)
- }
-
- else -> null
- }
- })
+ return Attachments(uris.mapNotNull { Attachment(it) })
}
@Provides
diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeView.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeView.kt
index af37016d..7d4f3d2e 100644
--- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeView.kt
+++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeView.kt
@@ -29,6 +29,15 @@ import io.reactivex.subjects.Subject
interface ComposeView : QkView {
+ companion object {
+ const val SelectContactRequestCode = 0
+ const val TakePhotoRequestCode = 1
+ const val AttachAFileRequestCode = 4
+ const val AttachContactRequestCode = 3
+
+ const val CameraDestinationKey = "camera_destination"
+ }
+
val activityVisibleIntent: Observable
val chipsSelectedIntent: Subject>
val chipDeletedIntent: Subject
@@ -43,10 +52,11 @@ interface ComposeView : QkView {
val textChangedIntent: Observable
val attachIntent: Observable
val cameraIntent: Observable<*>
- val galleryIntent: Observable<*>
+ val attachAnyFileIntent: Observable<*>
+ val attachImageFileIntent: Observable<*>
val scheduleIntent: Observable<*>
val attachContactIntent: Observable<*>
- val attachmentSelectedIntent: Observable
+ val attachAnyFileSelectedIntent: Observable
val contactSelectedIntent: Observable
val inputContentIntent: Observable
val scheduleSelectedIntent: Observable
@@ -68,7 +78,7 @@ interface ComposeView : QkView {
fun themeChanged()
fun showKeyboard()
fun requestCamera()
- fun requestGallery()
+ fun requestGallery(mimeType: String, requestCode: Int)
fun requestDatePicker()
fun requestContact()
fun setDraft(draft: String)
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 bf65a224..aa6b27ec 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
@@ -536,11 +536,17 @@ class ComposeViewModel @Inject constructor(
}
}
- // Attach a photo from gallery
- view.galleryIntent
- .doOnNext { newState { copy(attaching = false) } }
- .autoDisposable(view.scope())
- .subscribe { view.requestGallery() }
+ // pick a photo (specifically) from image provider apps
+ view.attachImageFileIntent
+ .doOnNext { newState { copy(attaching = false) } }
+ .autoDisposable(view.scope())
+ .subscribe { view.requestGallery("image/*", ComposeView.AttachAFileRequestCode) }
+
+ // pick any file from any provider apps
+ view.attachAnyFileIntent
+ .doOnNext { newState { copy(attaching = false) } }
+ .autoDisposable(view.scope())
+ .subscribe { view.requestGallery("*/*", ComposeView.AttachAFileRequestCode) }
// Choose a time to schedule the message
view.scheduleIntent
@@ -552,15 +558,15 @@ class ComposeViewModel @Inject constructor(
.autoDisposable(view.scope())
.subscribe { view.requestDatePicker() }
- // A photo was selected
+ // a file, photo or otherwise, was picked by the user
Observable.merge(
- view.attachmentSelectedIntent.map { uri -> Attachment.Image(uri) },
- view.inputContentIntent.map { inputContent -> Attachment.Image(inputContent = inputContent) })
- .withLatestFrom(attachments) { attachment, attachments -> attachments + attachment }
- .doOnNext(attachments::onNext)
- .autoDisposable(view.scope())
- .subscribe { newState { copy(attaching = false) } }
-
+ view.attachAnyFileSelectedIntent.map { uri -> Attachment(uri) },
+ view.inputContentIntent.map { inputContent -> Attachment(inputContent = inputContent) }
+ )
+ .withLatestFrom(attachments) { attachment, attachments -> attachments + attachment }
+ .doOnNext(attachments::onNext)
+ .autoDisposable(view.scope())
+ .subscribe { newState { copy(attaching = false) } }
// Set the scheduled time
view.scheduleSelectedIntent
.filter { scheduled ->
@@ -579,7 +585,7 @@ class ComposeViewModel @Inject constructor(
// Contact was selected for attachment
view.contactSelectedIntent
- .map { uri -> Attachment.Contact(getVCard(uri)!!) }
+ .map { uri -> Attachment(getFullVCardUri(uri)) }
.withLatestFrom(attachments) { attachment, attachments -> attachments + attachment }
.subscribeOn(Schedulers.io())
.autoDisposable(view.scope())
@@ -704,7 +710,6 @@ class ComposeViewModel @Inject constructor(
state.scheduled != 0L -> {
newState { copy(scheduled = 0) }
val uris = attachments
- .mapNotNull { it as? Attachment.Image }
.map { it.getUri() }
.map { it.toString() }
val params = AddScheduledMessage
@@ -784,17 +789,17 @@ class ComposeViewModel @Inject constructor(
}
- private fun getVCard(contactData: Uri): String? {
- val lookupKey = context.contentResolver.query(contactData, null, null, null, null)?.use { cursor ->
- cursor.moveToFirst()
- cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY))
+ private fun getFullVCardUri(contactData: Uri): Uri {
+ val lookupKey = context.contentResolver.query(contactData, null, null, null, null)?.use {
+ it.moveToFirst()
+ val index = it.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY)
+ if (index >= 0)
+ it.getString(index)
+ else
+ ""
}
- val vCardUri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_VCARD_URI, lookupKey)
- return context.contentResolver.openAssetFileDescriptor(vCardUri, "r")
- ?.createInputStream()
- ?.readBytes()
- ?.let { bytes -> String(bytes) }
+ return Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_VCARD_URI, lookupKey)
}
}
\ No newline at end of file
diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/scheduled/ScheduledMessageAttachmentAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/scheduled/ScheduledMessageAttachmentAdapter.kt
index 96a10ffc..e5f3e1b4 100644
--- a/presentation/src/main/java/com/moez/QKSMS/feature/scheduled/ScheduledMessageAttachmentAdapter.kt
+++ b/presentation/src/main/java/com/moez/QKSMS/feature/scheduled/ScheduledMessageAttachmentAdapter.kt
@@ -19,32 +19,112 @@
package dev.octoshrimpy.quik.feature.scheduled
import android.content.Context
+import android.content.Intent
+import android.media.MediaMetadataRetriever
import android.net.Uri
+import android.provider.MediaStore.MATCH_DEFAULT
+import android.provider.OpenableColumns
import android.view.LayoutInflater
+import android.view.View
import android.view.ViewGroup
+import com.bumptech.glide.Glide
import dev.octoshrimpy.quik.R
import dev.octoshrimpy.quik.common.base.QkAdapter
import dev.octoshrimpy.quik.common.base.QkViewHolder
-import dev.octoshrimpy.quik.util.GlideApp
-import kotlinx.android.synthetic.main.attachment_image_list_item.view.*
-import kotlinx.android.synthetic.main.scheduled_message_image_list_item.*
+import dev.octoshrimpy.quik.extensions.resourceExists
+import kotlinx.android.synthetic.main.scheduled_message_image_list_item.fileName
+import kotlinx.android.synthetic.main.scheduled_message_image_list_item.thumbnail
import javax.inject.Inject
+
class ScheduledMessageAttachmentAdapter @Inject constructor(
private val context: Context
) : QkAdapter() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QkViewHolder {
- val view = LayoutInflater.from(parent.context).inflate(R.layout.scheduled_message_image_list_item, parent, false)
- view.thumbnail.clipToOutline = true
-
- return QkViewHolder(view)
+ return QkViewHolder(
+ LayoutInflater
+ .from(parent.context)
+ .inflate(R.layout.scheduled_message_image_list_item, parent, false)
+ )
}
override fun onBindViewHolder(holder: QkViewHolder, position: Int) {
- val attachment = getItem(position)
+ val uri = getItem(position)
- GlideApp.with(context).load(attachment).into(holder.thumbnail)
+ // if uri target doesn't exist anymore
+ if (!uri.resourceExists(context)) {
+ holder.thumbnail.setImageResource(android.R.drawable.ic_delete)
+ holder.fileName.text = context.getString(R.string.attachment_missing)
+ holder.fileName.visibility = View.VISIBLE
+ return
+ }
+
+ val mimeType = context.contentResolver.getType(uri)
+
+ // if attachment mime type is image/* or video/*, use image/video-frame
+ if ((mimeType?.startsWith("image/") == true) ||
+ (mimeType?.startsWith("video/") == true)
+ ) {
+ Glide
+ .with(context)
+ .load(uri)
+ .into(holder.thumbnail)
+ return
+ }
+
+ // if audio mime type, try and use embedded image if one exists
+ if (mimeType?.startsWith("audio/") == true) {
+ val metaDataRetriever = MediaMetadataRetriever()
+ metaDataRetriever.setDataSource(context, uri)
+ val embeddedPicture = metaDataRetriever.embeddedPicture
+ if (embeddedPicture != null) {
+ Glide
+ .with(context)
+ .load(embeddedPicture)
+ .into(holder.thumbnail)
+ return
+ }
+ }
+
+ // try and use icon from default app for type
+ val intent = Intent(Intent.ACTION_VIEW).setDataAndType(uri, mimeType)
+ val appIcon = context
+ .packageManager
+ .resolveActivity(intent, MATCH_DEFAULT)
+ ?.loadIcon(context.packageManager)
+ if (appIcon != null)
+ Glide
+ .with(context)
+ .load(appIcon)
+ .into(holder.thumbnail)
+ else if (mimeType?.startsWith("audio/") == true)
+ // else, if audio, use default local audio icon
+ Glide
+ .with(context)
+ .load(R.drawable.ic_round_volume_up_24)
+ .into(holder.thumbnail)
+ else
+ // else, use default attachment icon
+ Glide
+ .with(context)
+ .load(R.drawable.ic_attachment_black_24dp)
+ .into(holder.thumbnail)
+
+ // else, show file name
+ val cursor = context.contentResolver.query(
+ uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null
+ )
+ cursor?.use {
+ try {
+ cursor.moveToFirst()
+ holder.fileName.text = cursor.getString(
+ cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) ?: 0
+ )
+ holder.fileName.visibility = View.VISIBLE
+ } catch (e: Exception) {
+ }
+ }
}
}
diff --git a/presentation/src/main/res/drawable/ic_round_volume_up_24.xml b/presentation/src/main/res/drawable/ic_round_volume_up_24.xml
new file mode 100644
index 00000000..cb631566
--- /dev/null
+++ b/presentation/src/main/res/drawable/ic_round_volume_up_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/presentation/src/main/res/layout/attachment_image_list_item.xml b/presentation/src/main/res/layout/attachment_file_list_item.xml
similarity index 82%
rename from presentation/src/main/res/layout/attachment_image_list_item.xml
rename to presentation/src/main/res/layout/attachment_file_list_item.xml
index ff757f03..b14e7827 100644
--- a/presentation/src/main/res/layout/attachment_image_list_item.xml
+++ b/presentation/src/main/res/layout/attachment_file_list_item.xml
@@ -37,7 +37,17 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
- tools:src="@tools:sample/avatars" />
+ tools:src="@tools:sample/backgrounds/scenic[0]" />
+
+
diff --git a/presentation/src/main/res/layout/compose_activity.xml b/presentation/src/main/res/layout/compose_activity.xml
index eac3add4..c8cf1ea5 100644
--- a/presentation/src/main/res/layout/compose_activity.xml
+++ b/presentation/src/main/res/layout/compose_activity.xml
@@ -204,7 +204,7 @@
android:layout_height="56dp"
android:padding="10dp"
android:src="@drawable/ic_cancel_black_24dp"
- android:tint="?android:attr/textColorSecondary"
+ app:tint="?android:attr/textColorSecondary"
app:layout_constraintBottom_toTopOf="@id/scheduledSeparator"
app:layout_constraintEnd_toEndOf="@id/messageBackground" />
@@ -230,7 +230,7 @@
app:layout_constraintBottom_toTopOf="@id/message"
app:layout_constraintEnd_toEndOf="@id/messageBackground"
app:layout_constraintStart_toStartOf="@id/messageBackground"
- tools:listitem="@layout/attachment_image_list_item" />
+ tools:listitem="@layout/attachment_file_list_item" />
@@ -304,7 +304,7 @@
android:contentDescription="@string/compose_send_cd"
android:padding="10dp"
android:src="@drawable/ic_send_black_24dp"
- android:tint="?android:textColorSecondary"
+ app:tint="?android:textColorSecondary"
app:layout_constraintBottom_toBottomOf="@id/messageBackground"
app:layout_constraintEnd_toEndOf="parent" />
@@ -371,7 +371,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
- app:constraint_referenced_ids="contact,contactLabel,schedule,scheduleLabel,gallery,galleryLabel,camera,cameraLabel,attachingBackground" />
+ app:constraint_referenced_ids="contact,contactLabel,schedule,scheduleLabel,gallery,galleryLabel,attachAFileIcon,attachAFileLabel,camera,cameraLabel,attachingBackground" />
@@ -452,6 +452,43 @@
app:layout_constraintStart_toEndOf="@id/schedule"
app:layout_constraintTop_toTopOf="@id/schedule" />
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/presentation/src/main/res/values-el/strings.xml b/presentation/src/main/res/values-el/strings.xml
index 9e8a538b..06ac0917 100644
--- a/presentation/src/main/res/values-el/strings.xml
+++ b/presentation/src/main/res/values-el/strings.xml
@@ -250,7 +250,7 @@
Send long messages as MMS
If your longer text messages are failing to send, or sending in the wrong order, you can send them as MMS messages instead. Additional charges may apply
- Auto-compress MMS attachments
+ Auto-compress MMS image attachments
Συγχρονισμός μηνυμάτων
Επανασυγχρονισμός μηνυμάτων με τη βάση δεδομένων SMS του Android
Σχετικά με το QUIK
diff --git a/presentation/src/main/res/values-gd/strings.xml b/presentation/src/main/res/values-gd/strings.xml
index e9e43c73..8c790a8e 100644
--- a/presentation/src/main/res/values-gd/strings.xml
+++ b/presentation/src/main/res/values-gd/strings.xml
@@ -254,7 +254,7 @@
Send long messages as MMS
If your longer text messages are failing to send, or sending in the wrong order, you can send them as MMS messages instead. Additional charges may apply
- Auto-compress MMS attachments
+ Auto-compress MMS image attachments
Sync messages
Re-sync your messages with the native Android SMS database
About QUIK
diff --git a/presentation/src/main/res/values-lt/strings.xml b/presentation/src/main/res/values-lt/strings.xml
index 22e2c9a0..ab1559e0 100644
--- a/presentation/src/main/res/values-lt/strings.xml
+++ b/presentation/src/main/res/values-lt/strings.xml
@@ -254,7 +254,7 @@
Send long messages as MMS
If your longer text messages are failing to send, or sending in the wrong order, you can send them as MMS messages instead. Additional charges may apply
- Auto-compress MMS attachments
+ Auto-compress MMS image attachments
Sinchronizuoti pranešimus
Iš naujo sinchronizuoti Android pranešimų duomenų bazę
Apie QUIK
diff --git a/presentation/src/main/res/values-ne/strings.xml b/presentation/src/main/res/values-ne/strings.xml
index 5921fd84..cad960c0 100644
--- a/presentation/src/main/res/values-ne/strings.xml
+++ b/presentation/src/main/res/values-ne/strings.xml
@@ -250,7 +250,7 @@
Send long messages as MMS
If your longer text messages are failing to send, or sending in the wrong order, you can send them as MMS messages instead. Additional charges may apply
- Auto-compress MMS attachments
+ Auto-compress MMS image attachments
Sync messages
Re-sync your messages with the native Android SMS database
About QUIK
diff --git a/presentation/src/main/res/values-ta/strings.xml b/presentation/src/main/res/values-ta/strings.xml
index 1319f51c..75b60836 100644
--- a/presentation/src/main/res/values-ta/strings.xml
+++ b/presentation/src/main/res/values-ta/strings.xml
@@ -250,7 +250,7 @@
நீண்ட செய்திகளை எம்.எம்.எஸ் ஆக அனுப்பு
If your longer text messages are failing to send, or sending in the wrong order, you can send them as MMS messages instead. Additional charges may apply
- Auto-compress MMS attachments
+ Auto-compress MMS image attachments
தகவல்களை ஒத்திசை
Re-sync your messages with the native Android SMS database
QUIK பற்றி
diff --git a/presentation/src/main/res/values-tl/strings.xml b/presentation/src/main/res/values-tl/strings.xml
index 3940f2bc..c156eaff 100644
--- a/presentation/src/main/res/values-tl/strings.xml
+++ b/presentation/src/main/res/values-tl/strings.xml
@@ -250,7 +250,7 @@
Send long messages as MMS
If your longer text messages are failing to send, or sending in the wrong order, you can send them as MMS messages instead. Additional charges may apply
- Auto-compress MMS attachments
+ Auto-compress MMS image attachments
Sync messages
Re-sync your messages with the native Android SMS database
About QUIK
diff --git a/presentation/src/main/res/values-ur/strings.xml b/presentation/src/main/res/values-ur/strings.xml
index 731e6f6e..029c80cc 100644
--- a/presentation/src/main/res/values-ur/strings.xml
+++ b/presentation/src/main/res/values-ur/strings.xml
@@ -250,7 +250,7 @@
Send long messages as MMS
If your longer text messages are failing to send, or sending in the wrong order, you can send them as MMS messages instead. Additional charges may apply
- Auto-compress MMS attachments
+ Auto-compress MMS image attachments
Sync messages
Re-sync your messages with the native Android SMS database
About QUIK
diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml
index 99f8d9b6..9e61419d 100644
--- a/presentation/src/main/res/values/strings.xml
+++ b/presentation/src/main/res/values/strings.xml
@@ -141,6 +141,7 @@
Delivered: %s
Error code: %d
Add an attachment
+ Attach any file
Attach a photo
Take a photo
Schedule message
@@ -211,6 +212,7 @@
Sending on December 23rd
Schedule a message
Scheduled message
+ Missing
- Send now
- Copy text
@@ -284,7 +286,7 @@
Send long messages as MMS
If your longer text messages are failing to send, or sending in the wrong order, you can send them as MMS messages instead. Additional charges may apply
- Auto-compress MMS attachments
+ Auto-compress MMS image attachments
Sync messages
Re-sync your messages with the native Android SMS database
About QUIK