forked from upstream-mirrors/quik
Move packages to com.moez.QKSMS
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.moez.QKSMS.compat
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.os.Build
|
||||
import android.telephony.SubscriptionInfo
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP_MR1)
|
||||
data class SubscriptionInfoCompat(private val subscriptionInfo: SubscriptionInfo) {
|
||||
|
||||
val subscriptionId get() = subscriptionInfo.subscriptionId
|
||||
|
||||
val simSlotIndex get() = subscriptionInfo.simSlotIndex
|
||||
|
||||
val displayName get() = subscriptionInfo.displayName
|
||||
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.moez.QKSMS.compat
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.telephony.SubscriptionManager
|
||||
import com.moez.QKSMS.manager.PermissionManager
|
||||
import javax.inject.Inject
|
||||
|
||||
class SubscriptionManagerCompat @Inject constructor(context: Context, private val permissions: PermissionManager) {
|
||||
|
||||
private val subscriptionManager: SubscriptionManager?
|
||||
get() = field?.takeIf { permissions.hasPhone() }
|
||||
|
||||
val activeSubscriptionInfoList: List<SubscriptionInfoCompat>
|
||||
get() {
|
||||
return if (Build.VERSION.SDK_INT >= 22) {
|
||||
subscriptionManager?.activeSubscriptionInfoList?.map { SubscriptionInfoCompat(it) } ?: listOf()
|
||||
} else listOf()
|
||||
}
|
||||
|
||||
init {
|
||||
subscriptionManager = if (Build.VERSION.SDK_INT >= 22) SubscriptionManager.from(context) else null
|
||||
}
|
||||
|
||||
fun addOnSubscriptionsChangedListener(listener: OnSubscriptionsChangedListener) {
|
||||
if (Build.VERSION.SDK_INT >= 22) {
|
||||
subscriptionManager?.addOnSubscriptionsChangedListener(listener.listener)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeOnSubscriptionsChangedListener(listener: OnSubscriptionsChangedListener) {
|
||||
if (Build.VERSION.SDK_INT >= 22) {
|
||||
subscriptionManager?.removeOnSubscriptionsChangedListener(listener.listener)
|
||||
}
|
||||
}
|
||||
|
||||
abstract class OnSubscriptionsChangedListener {
|
||||
|
||||
val listener: SubscriptionManager.OnSubscriptionsChangedListener? = if (Build.VERSION.SDK_INT >= 22) {
|
||||
object : SubscriptionManager.OnSubscriptionsChangedListener() {
|
||||
override fun onSubscriptionsChanged() {
|
||||
this@OnSubscriptionsChangedListener.onSubscriptionsChanged()
|
||||
}
|
||||
}
|
||||
} else null
|
||||
|
||||
abstract fun onSubscriptionsChanged()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.moez.QKSMS.compat
|
||||
|
||||
import android.content.Context
|
||||
import android.database.sqlite.SqliteWrapper
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.BaseColumns
|
||||
import android.provider.Telephony
|
||||
import android.text.TextUtils
|
||||
import android.util.Patterns
|
||||
import timber.log.Timber
|
||||
import java.util.regex.Pattern
|
||||
|
||||
object TelephonyCompat {
|
||||
|
||||
private val ID_PROJECTION = arrayOf(BaseColumns._ID)
|
||||
|
||||
private val THREAD_ID_CONTENT_URI = Uri.parse("content://mms-sms/threadID")
|
||||
|
||||
val NAME_ADDR_EMAIL_PATTERN = Pattern.compile("\\s*(\"[^\"]*\"|[^<>\"]+)\\s*<([^<>]+)>\\s*")
|
||||
|
||||
fun getOrCreateThreadId(context: Context, recipient: String): Long {
|
||||
return getOrCreateThreadId(context, listOf(recipient))
|
||||
}
|
||||
|
||||
fun getOrCreateThreadId(context: Context, recipients: Collection<String>): Long {
|
||||
return if (Build.VERSION.SDK_INT >= 23) {
|
||||
Telephony.Threads.getOrCreateThreadId(context, recipients.toSet())
|
||||
} else {
|
||||
val uriBuilder = THREAD_ID_CONTENT_URI.buildUpon()
|
||||
|
||||
recipients
|
||||
.map { recipient -> if (isEmailAddress(recipient)) extractAddrSpec(recipient) else recipient }
|
||||
.forEach { recipient -> uriBuilder.appendQueryParameter("recipient", recipient) }
|
||||
|
||||
val uri = uriBuilder.build()
|
||||
|
||||
SqliteWrapper.query(context, context.contentResolver, uri, ID_PROJECTION, null, null, null)?.use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
return cursor.getLong(0)
|
||||
} else {
|
||||
Timber.e("getOrCreateThreadId returned no rows!")
|
||||
}
|
||||
}
|
||||
|
||||
Timber.e("getOrCreateThreadId failed with " + recipients.size + " recipients")
|
||||
throw IllegalArgumentException("Unable to find or allocate a thread ID.")
|
||||
}
|
||||
}
|
||||
|
||||
fun extractAddrSpec(address: String): String {
|
||||
val match = NAME_ADDR_EMAIL_PATTERN.matcher(address)
|
||||
return if (match.matches()) {
|
||||
match.group(2)
|
||||
} else address
|
||||
}
|
||||
|
||||
fun isEmailAddress(address: String): Boolean {
|
||||
if (TextUtils.isEmpty(address)) {
|
||||
return false
|
||||
}
|
||||
val s = extractAddrSpec(address)
|
||||
val match = Patterns.EMAIL_ADDRESS.matcher(s)
|
||||
return match.matches()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.moez.QKSMS.extensions
|
||||
|
||||
import android.database.Cursor
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.Maybe
|
||||
import io.reactivex.subjects.MaybeSubject
|
||||
|
||||
fun Cursor.forEach(closeOnComplete: Boolean = true, method: (Cursor) -> Unit = {}) {
|
||||
moveToPosition(-1)
|
||||
while (moveToNext()) {
|
||||
method.invoke(this)
|
||||
}
|
||||
|
||||
if (closeOnComplete) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> Cursor.map(map: (Cursor) -> T): List<T> {
|
||||
return List(count, { position ->
|
||||
moveToPosition(position)
|
||||
map(this)
|
||||
})
|
||||
}
|
||||
|
||||
fun <T> Cursor.mapWhile(map: (Cursor) -> T, predicate: (T) -> Boolean): ArrayList<T> {
|
||||
val result = ArrayList<T>()
|
||||
|
||||
moveToPosition(-1)
|
||||
while (moveToNext()) {
|
||||
val item = map(this)
|
||||
|
||||
if (!predicate(item)) break
|
||||
|
||||
result.add(item)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* We're using this simple implementation with .range() because of the
|
||||
* complexities of dealing with Backpressure with a Cursor. We can't simply
|
||||
* use a loop and call onNext() from a generator because we'll need to close
|
||||
* the cursor at the end, and if any items are still in the buffer, then
|
||||
* they will be made invalid
|
||||
*/
|
||||
fun Cursor.asFlowable(): Flowable<Cursor> {
|
||||
return Flowable.range(0, count)
|
||||
.map {
|
||||
moveToPosition(it)
|
||||
this
|
||||
}
|
||||
.doOnComplete { close() }
|
||||
}
|
||||
|
||||
fun Cursor.asMaybe(): Maybe<Cursor> {
|
||||
val subject = MaybeSubject.create<Cursor>()
|
||||
|
||||
if (moveToFirst()) {
|
||||
subject.onSuccess(this)
|
||||
} else {
|
||||
subject.onError(IndexOutOfBoundsException("The cursor has no items"))
|
||||
}
|
||||
|
||||
subject.doOnComplete { close() }
|
||||
return subject
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.moez.QKSMS.extensions
|
||||
|
||||
import com.google.android.mms.ContentType
|
||||
import com.moez.QKSMS.model.MmsPart
|
||||
|
||||
fun MmsPart.isSmil() = ContentType.APP_SMIL == type
|
||||
|
||||
fun MmsPart.isImage() = ContentType.isImageType(type)
|
||||
|
||||
fun MmsPart.isVideo() = ContentType.isVideoType(type)
|
||||
|
||||
fun MmsPart.isText() = ContentType.isTextType(type)
|
||||
|
||||
fun MmsPart.isVCard() = ContentType.TEXT_VCARD == type
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.moez.QKSMS.extensions
|
||||
|
||||
import io.reactivex.Observable
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmModel
|
||||
import io.realm.RealmObject
|
||||
import io.realm.RealmQuery
|
||||
import io.realm.RealmResults
|
||||
import timber.log.Timber
|
||||
|
||||
fun RealmModel.insertOrUpdate() {
|
||||
val realm = Realm.getDefaultInstance()
|
||||
realm.executeTransaction { realm.insertOrUpdate(this) }
|
||||
realm.close()
|
||||
}
|
||||
|
||||
fun <T : RealmModel> Collection<T>.insertOrUpdate() {
|
||||
val realm = Realm.getDefaultInstance()
|
||||
realm.executeTransaction { realm.insertOrUpdate(this) }
|
||||
realm.close()
|
||||
}
|
||||
|
||||
fun <T : RealmObject> T.asObservable(): Observable<T> {
|
||||
return asFlowable<T>().toObservable()
|
||||
}
|
||||
|
||||
fun <T : RealmObject> RealmResults<T>.asObservable(): Observable<RealmResults<T>> {
|
||||
return asFlowable().toObservable()
|
||||
}
|
||||
|
||||
fun <T : RealmObject> RealmQuery<T>.anyOf(fieldName: String, values: LongArray): RealmQuery<T> {
|
||||
values.forEach { Timber.v("vararg: $it") }
|
||||
|
||||
return when (values.isEmpty()) {
|
||||
true -> equalTo(fieldName, -1L)
|
||||
false -> `in`(fieldName, values.toTypedArray())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.moez.QKSMS.extensions
|
||||
|
||||
import java.text.Normalizer
|
||||
|
||||
/**
|
||||
* Strip the accents from a string
|
||||
*/
|
||||
fun CharSequence.removeAccents() = Normalizer.normalize(this, Normalizer.Form.NFD)
|
||||
.replace(Regex("[^\\p{ASCII}]"), "")
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.moez.QKSMS.filter
|
||||
|
||||
import com.moez.QKSMS.extensions.removeAccents
|
||||
import com.moez.QKSMS.model.Contact
|
||||
import javax.inject.Inject
|
||||
|
||||
class ContactFilter @Inject constructor(private val phoneNumberFilter: PhoneNumberFilter) : Filter<Contact>() {
|
||||
|
||||
override fun filter(item: Contact, query: CharSequence): Boolean {
|
||||
return item.name.removeAccents().contains(query, true) || // Name
|
||||
item.numbers.map { it.address }.any { address -> phoneNumberFilter.filter(address, query) } // Number
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.moez.QKSMS.filter
|
||||
|
||||
import com.moez.QKSMS.model.Conversation
|
||||
import javax.inject.Inject
|
||||
|
||||
class ConversationFilter @Inject constructor(private val recipientFilter: RecipientFilter) : Filter<Conversation>() {
|
||||
|
||||
override fun filter(item: Conversation, query: CharSequence): Boolean {
|
||||
return item.recipients.any { recipient -> recipientFilter.filter(recipient, query) }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.moez.QKSMS.filter
|
||||
|
||||
abstract class Filter<in T> {
|
||||
|
||||
abstract fun filter(item: T, query: CharSequence): Boolean
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.moez.QKSMS.filter
|
||||
|
||||
import android.telephony.PhoneNumberUtils
|
||||
import javax.inject.Inject
|
||||
|
||||
class PhoneNumberFilter @Inject constructor() : Filter<String>() {
|
||||
|
||||
override fun filter(item: String, query: CharSequence): Boolean {
|
||||
val allCharactersDialable = query.all { PhoneNumberUtils.isReallyDialable(it) }
|
||||
|
||||
return allCharactersDialable && (PhoneNumberUtils.compare(item, query.toString()) ||
|
||||
PhoneNumberUtils.stripSeparators(item).contains(PhoneNumberUtils.stripSeparators(query.toString())))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.moez.QKSMS.filter
|
||||
|
||||
import com.moez.QKSMS.model.Recipient
|
||||
import javax.inject.Inject
|
||||
|
||||
class RecipientFilter @Inject constructor(
|
||||
private val contactFilter: ContactFilter,
|
||||
private val phoneNumberFilter: PhoneNumberFilter)
|
||||
: Filter<Recipient>() {
|
||||
|
||||
override fun filter(item: Recipient, query: CharSequence) = when {
|
||||
item.contact?.let { contactFilter.filter(it, query) } == true -> true
|
||||
phoneNumberFilter.filter(item.address, query) -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.moez.QKSMS.manager
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.os.Message
|
||||
import android.os.Messenger
|
||||
import androidx.os.bundleOf
|
||||
import com.moez.QKSMS.util.Preferences
|
||||
import com.moez.QKSMS.util.tryOrNull
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.subjects.SingleSubject
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class ExternalBlockingManagerImpl @Inject constructor(
|
||||
private val context: Context,
|
||||
private val prefs: Preferences
|
||||
) : ExternalBlockingManager {
|
||||
|
||||
companion object {
|
||||
const val RATING_UNKNOWN = 0
|
||||
const val RATING_POSITIVE = 1
|
||||
const val RATING_NEGATIVE = 2
|
||||
const val RATING_NEUTRAL = 3
|
||||
|
||||
const val GET_NUMBER_RATING = 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a Single<Boolean> which emits whether or not the given [address] should be blocked
|
||||
*/
|
||||
override fun shouldBlock(address: String): Single<Boolean> {
|
||||
return Binder(context, prefs, address).shouldBlock()
|
||||
}
|
||||
|
||||
private class Binder(
|
||||
private val context: Context,
|
||||
private val prefs: Preferences,
|
||||
private val address: String
|
||||
) : ServiceConnection {
|
||||
|
||||
private val subject: SingleSubject<Boolean> = SingleSubject.create()
|
||||
private var serviceMessenger: Messenger? = null
|
||||
private var isBound: Boolean = false
|
||||
|
||||
fun shouldBlock(): Single<Boolean> {
|
||||
|
||||
var intent: Intent? = null
|
||||
|
||||
// If either version of Should I Answer? is installed and SIA is enabled, build the
|
||||
// intent to request a rating
|
||||
if (prefs.sia.get()) {
|
||||
intent = tryOrNull {
|
||||
context.packageManager.getApplicationInfo("org.mistergroup.shouldianswerpersonal", 0).enabled
|
||||
Intent("org.mistergroup.shouldianswerpersonal.PublicService").setPackage("org.mistergroup.shouldianswerpersonal")
|
||||
} ?: tryOrNull {
|
||||
context.packageManager.getApplicationInfo("org.mistergroup.muzutozvednout", 0).enabled
|
||||
Intent("org.mistergroup.muzutozvednout.PublicService").setPackage("org.mistergroup.muzutozvednout")
|
||||
}
|
||||
}
|
||||
|
||||
// If the intent isn't null, bind the service and wait for a result. Otherwise, don't block
|
||||
if (intent != null) {
|
||||
context.bindService(intent, this, Context.BIND_AUTO_CREATE)
|
||||
} else {
|
||||
subject.onSuccess(false)
|
||||
}
|
||||
|
||||
return subject
|
||||
}
|
||||
|
||||
override fun onServiceConnected(className: ComponentName, service: IBinder) {
|
||||
serviceMessenger = Messenger(service)
|
||||
isBound = true
|
||||
|
||||
val msg = Message()
|
||||
msg.what = GET_NUMBER_RATING
|
||||
msg.data = bundleOf(Pair("number", address))
|
||||
msg.replyTo = Messenger(IncomingHandler { rating ->
|
||||
subject.onSuccess(rating == RATING_NEGATIVE)
|
||||
Timber.v("Should block: ${rating == RATING_NEGATIVE}")
|
||||
|
||||
// We're done, so unbind the service
|
||||
if (isBound && serviceMessenger != null) {
|
||||
context.unbindService(this)
|
||||
}
|
||||
})
|
||||
|
||||
serviceMessenger?.send(msg)
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(className: ComponentName) {
|
||||
serviceMessenger = null
|
||||
isBound = false
|
||||
}
|
||||
}
|
||||
|
||||
private class IncomingHandler(private val callback: (rating: Int) -> Unit) : Handler() {
|
||||
|
||||
override fun handleMessage(msg: Message) {
|
||||
when (msg.what) {
|
||||
GET_NUMBER_RATING -> callback(msg.data.getInt("rating"))
|
||||
else -> super.handleMessage(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.moez.QKSMS.manager
|
||||
|
||||
import com.moez.QKSMS.model.Message
|
||||
import io.realm.Realm
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class KeyManagerImpl @Inject constructor() : KeyManager {
|
||||
|
||||
private var initialized = false
|
||||
private var maxValue: Long = 0
|
||||
|
||||
/**
|
||||
* Should be called when a new sync is being started
|
||||
*/
|
||||
override fun reset() {
|
||||
initialized = true
|
||||
maxValue = 0L
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a valid ID that can be used to store a new message
|
||||
*/
|
||||
override fun newId(): Long {
|
||||
if (!initialized) {
|
||||
maxValue = Realm.getDefaultInstance().use { realm ->
|
||||
realm.where(Message::class.java).max("id")?.toLong() ?: 0L
|
||||
}
|
||||
initialized = true
|
||||
}
|
||||
|
||||
maxValue++
|
||||
return maxValue
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.moez.QKSMS.manager
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||
import android.provider.Telephony
|
||||
import android.support.v4.content.ContextCompat
|
||||
import javax.inject.Inject
|
||||
|
||||
class PermissionManagerImpl @Inject constructor(private val context: Context) : PermissionManager {
|
||||
|
||||
override fun isDefaultSms(): Boolean {
|
||||
return Telephony.Sms.getDefaultSmsPackage(context) == context.packageName
|
||||
}
|
||||
|
||||
override fun hasSmsAndContacts(): Boolean {
|
||||
return hasSms() && hasContacts()
|
||||
}
|
||||
|
||||
override fun hasSms(): Boolean = ContextCompat.checkSelfPermission(context, Manifest.permission.READ_SMS) == PERMISSION_GRANTED
|
||||
|
||||
override fun hasContacts(): Boolean = ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS) == PERMISSION_GRANTED
|
||||
|
||||
override fun hasPhone(): Boolean = ContextCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_STATE) == PERMISSION_GRANTED
|
||||
|
||||
override fun hasStorage(): Boolean = ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PERMISSION_GRANTED
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.moez.QKSMS.manager
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.klinker.android.send_message.BroadcastUtils
|
||||
import me.leolin.shortcutbadger.ShortcutBadger
|
||||
import javax.inject.Inject
|
||||
|
||||
class WidgetManagerImpl @Inject constructor(private val context: Context) : WidgetManager {
|
||||
|
||||
override fun updateUnreadCount(count: Int) {
|
||||
ShortcutBadger.applyCount(context, count)
|
||||
|
||||
BroadcastUtils.sendExplicitBroadcast(context, Intent(), WidgetManager.ACTION_NOTIFY_DATASET_CHANGED)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.moez.QKSMS.mapper
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.provider.ContactsContract.CommonDataKinds.Phone.*
|
||||
import com.moez.QKSMS.manager.PermissionManager
|
||||
import com.moez.QKSMS.model.Contact
|
||||
import com.moez.QKSMS.model.PhoneNumber
|
||||
import javax.inject.Inject
|
||||
|
||||
class CursorToContactImpl @Inject constructor(
|
||||
private val context: Context,
|
||||
private val permissionManager: PermissionManager
|
||||
) : CursorToContact {
|
||||
|
||||
companion object {
|
||||
val URI = CONTENT_URI
|
||||
val PROJECTION = arrayOf(LOOKUP_KEY, NUMBER, TYPE, DISPLAY_NAME)
|
||||
|
||||
const val COLUMN_LOOKUP_KEY = 0
|
||||
const val COLUMN_NUMBER = 1
|
||||
const val COLUMN_TYPE = 2
|
||||
const val COLUMN_DISPLAY_NAME = 3
|
||||
}
|
||||
|
||||
override fun map(from: Cursor) = Contact().apply {
|
||||
lookupKey = from.getString(COLUMN_LOOKUP_KEY)
|
||||
name = from.getString(COLUMN_DISPLAY_NAME) ?: ""
|
||||
numbers.add(PhoneNumber().apply {
|
||||
address = from.getString(COLUMN_NUMBER) ?: ""
|
||||
type = context.getString(getTypeLabelResource(from.getInt(COLUMN_TYPE)))
|
||||
})
|
||||
lastUpdate = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
override fun getContactsCursor(): Cursor? {
|
||||
return when (permissionManager.hasContacts()) {
|
||||
true -> context.contentResolver.query(URI, PROJECTION, null, null, null)
|
||||
false -> null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.moez.QKSMS.mapper
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.provider.Telephony.Threads
|
||||
import com.moez.QKSMS.manager.PermissionManager
|
||||
import com.moez.QKSMS.model.Conversation
|
||||
import com.moez.QKSMS.model.Recipient
|
||||
import javax.inject.Inject
|
||||
|
||||
class CursorToConversationImpl @Inject constructor(
|
||||
private val context: Context,
|
||||
private val permissionManager: PermissionManager
|
||||
) : CursorToConversation {
|
||||
|
||||
companion object {
|
||||
val URI: Uri = Uri.parse("content://mms-sms/conversations?simple=true")
|
||||
val PROJECTION = arrayOf(
|
||||
Threads._ID,
|
||||
Threads.DATE,
|
||||
Threads.RECIPIENT_IDS,
|
||||
Threads.MESSAGE_COUNT,
|
||||
Threads.READ,
|
||||
Threads.SNIPPET)
|
||||
|
||||
const val ID = 0
|
||||
const val DATE = 1
|
||||
const val RECIPIENT_IDS = 2
|
||||
const val MESSAGE_COUNT = 3
|
||||
const val READ = 4
|
||||
const val SNIPPET = 5
|
||||
}
|
||||
|
||||
override fun map(from: Cursor): Conversation {
|
||||
return Conversation().apply {
|
||||
id = from.getLong(ID)
|
||||
date = from.getLong(DATE)
|
||||
recipients.addAll(from.getString(RECIPIENT_IDS)
|
||||
.split(" ")
|
||||
.filter { it.isNotBlank() }
|
||||
.map { recipientId -> recipientId.toLong() }
|
||||
.map { recipientId -> Recipient().apply { id = recipientId } })
|
||||
count = from.getInt(MESSAGE_COUNT)
|
||||
read = from.getInt(READ) == 1
|
||||
snippet = from.getString(SNIPPET) ?: ""
|
||||
}
|
||||
}
|
||||
|
||||
override fun getConversationsCursor(lastSync: Long): Cursor? {
|
||||
return when (permissionManager.hasSms()) {
|
||||
true -> context.contentResolver.query(URI, PROJECTION,
|
||||
"date > $lastSync", null,
|
||||
"date desc")
|
||||
|
||||
false -> null
|
||||
}
|
||||
}
|
||||
|
||||
override fun getConversationCursor(threadId: Long): Cursor? {
|
||||
return context.contentResolver.query(URI, PROJECTION,
|
||||
"_id = ?", arrayOf(threadId.toString()), null)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.moez.QKSMS.mapper
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.provider.Telephony.*
|
||||
import com.google.android.mms.pdu_alt.PduHeaders
|
||||
import com.moez.QKSMS.extensions.map
|
||||
import com.moez.QKSMS.manager.KeyManager
|
||||
import com.moez.QKSMS.manager.PermissionManager
|
||||
import com.moez.QKSMS.model.Message
|
||||
import javax.inject.Inject
|
||||
|
||||
class CursorToMessageImpl @Inject constructor(
|
||||
private val context: Context,
|
||||
private val cursorToPart: CursorToPart,
|
||||
private val keys: KeyManager,
|
||||
private val permissionManager: PermissionManager
|
||||
) : CursorToMessage {
|
||||
|
||||
companion object {
|
||||
val URI = Uri.parse("content://mms-sms/complete-conversations")
|
||||
val PROJECTION = arrayOf(
|
||||
MmsSms.TYPE_DISCRIMINATOR_COLUMN,
|
||||
MmsSms._ID,
|
||||
Mms.DATE,
|
||||
Mms.DATE_SENT,
|
||||
Mms.READ,
|
||||
Mms.THREAD_ID,
|
||||
Mms.LOCKED,
|
||||
Mms.SUBSCRIPTION_ID,
|
||||
|
||||
Sms.ADDRESS,
|
||||
Sms.BODY,
|
||||
Sms.SEEN,
|
||||
Sms.TYPE,
|
||||
Sms.STATUS,
|
||||
Sms.ERROR_CODE,
|
||||
|
||||
Mms.SUBJECT,
|
||||
Mms.SUBJECT_CHARSET,
|
||||
Mms.SEEN,
|
||||
Mms.MESSAGE_TYPE,
|
||||
Mms.MESSAGE_BOX,
|
||||
Mms.DELIVERY_REPORT,
|
||||
Mms.READ_REPORT,
|
||||
MmsSms.PendingMessages.ERROR_TYPE,
|
||||
Mms.STATUS)
|
||||
}
|
||||
|
||||
override fun map(from: Pair<Cursor, CursorToMessage.MessageColumns>): Message {
|
||||
val cursor = from.first
|
||||
val columnsMap = from.second
|
||||
|
||||
return Message().apply {
|
||||
type = when {
|
||||
cursor.getColumnIndex(MmsSms.TYPE_DISCRIMINATOR_COLUMN) != -1 -> cursor.getString(columnsMap.msgType)
|
||||
cursor.getColumnIndex(Mms.SUBJECT) != -1 -> "mms"
|
||||
cursor.getColumnIndex(Sms.ADDRESS) != -1 -> "sms"
|
||||
else -> "unknown"
|
||||
}
|
||||
|
||||
id = keys.newId()
|
||||
threadId = cursor.getLong(columnsMap.threadId)
|
||||
contentId = cursor.getLong(columnsMap.msgId)
|
||||
date = cursor.getLong(columnsMap.date)
|
||||
dateSent = cursor.getLong(columnsMap.dateSent)
|
||||
read = cursor.getInt(columnsMap.read) != 0
|
||||
locked = cursor.getInt(columnsMap.locked) != 0
|
||||
subId = if (columnsMap.subId != -1) cursor.getInt(columnsMap.subId) else -1
|
||||
|
||||
when (type) {
|
||||
"sms" -> {
|
||||
address = cursor.getString(columnsMap.smsAddress) ?: ""
|
||||
boxId = cursor.getInt(columnsMap.smsType)
|
||||
seen = cursor.getInt(columnsMap.smsSeen) != 0
|
||||
|
||||
body = columnsMap.smsBody
|
||||
.takeIf { column -> column != -1 } // The column may not be set
|
||||
?.let { column -> cursor.getString(column) } ?: "" // cursor.getString() may return null
|
||||
|
||||
errorCode = cursor.getInt(columnsMap.smsErrorCode)
|
||||
deliveryStatus = cursor.getInt(columnsMap.smsStatus)
|
||||
}
|
||||
|
||||
"mms" -> {
|
||||
address = getMmsAddress(contentId)
|
||||
boxId = cursor.getInt(columnsMap.mmsMessageBox)
|
||||
date *= 1000L
|
||||
dateSent *= 1000L
|
||||
seen = cursor.getInt(columnsMap.mmsSeen) != 0
|
||||
mmsDeliveryStatusString = cursor.getString(columnsMap.mmsDeliveryReport) ?: ""
|
||||
errorType = if (columnsMap.mmsErrorType != -1) cursor.getInt(columnsMap.mmsErrorType) else 0
|
||||
messageSize = 0
|
||||
readReportString = cursor.getString(columnsMap.mmsReadReport) ?: ""
|
||||
messageType = cursor.getInt(columnsMap.mmsMessageType)
|
||||
mmsStatus = cursor.getInt(columnsMap.mmsStatus)
|
||||
subject = cursor.getString(columnsMap.mmsSubject) ?: ""
|
||||
textContentType = ""
|
||||
attachmentType = Message.AttachmentType.NOT_LOADED
|
||||
|
||||
parts.addAll(cursorToPart.getPartsCursor(contentId)?.map { cursorToPart.map(it) } ?: listOf())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getMessagesCursor(): Cursor? {
|
||||
return when (permissionManager.hasSms()) {
|
||||
true -> context.contentResolver.query(URI, PROJECTION, null, null, "normalized_date desc")
|
||||
false -> null
|
||||
}
|
||||
}
|
||||
|
||||
override fun getMessageCursor(id: Long): Cursor? {
|
||||
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
|
||||
}
|
||||
|
||||
private fun getMmsAddress(messageId: Long): String {
|
||||
val uri = Mms.CONTENT_URI.buildUpon()
|
||||
.appendPath(messageId.toString())
|
||||
.appendPath("addr").build()
|
||||
|
||||
//TODO: Use Charset to ensure address is decoded correctly
|
||||
val projection = arrayOf(Mms.Addr.ADDRESS, Mms.Addr.CHARSET)
|
||||
val selection = "${Mms.Addr.TYPE} = ${PduHeaders.FROM}"
|
||||
|
||||
val cursor = context.contentResolver.query(uri, projection, selection, null, null)
|
||||
cursor?.use {
|
||||
if (cursor.moveToFirst()) {
|
||||
return cursor.getString(0) ?: ""
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.moez.QKSMS.mapper
|
||||
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.provider.Telephony
|
||||
import com.moez.QKSMS.extensions.isImage
|
||||
import com.moez.QKSMS.extensions.isSmil
|
||||
import com.moez.QKSMS.extensions.isText
|
||||
import com.moez.QKSMS.extensions.isVideo
|
||||
import com.moez.QKSMS.model.MmsPart
|
||||
import timber.log.Timber
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import javax.inject.Inject
|
||||
|
||||
class CursorToPartImpl @Inject constructor(private val context: Context) : CursorToPart {
|
||||
|
||||
companion object {
|
||||
val CONTENT_URI: Uri = Uri.parse("content://mms/part")
|
||||
}
|
||||
|
||||
override fun map(from: Cursor) = MmsPart().apply {
|
||||
id = from.getLong(from.getColumnIndexOrThrow(Telephony.Mms.Part._ID))
|
||||
|
||||
// Type will sometimes return null, resulting in a crash if we don't default to an empty string
|
||||
type = from.getString(from.getColumnIndexOrThrow(Telephony.Mms.Part.CONTENT_TYPE)) ?: ""
|
||||
|
||||
val data = from.getString(from.getColumnIndexOrThrow(Telephony.Mms.Part._DATA))
|
||||
|
||||
when {
|
||||
isSmil() || isImage() || isVideo() -> {
|
||||
// Do nothing special
|
||||
}
|
||||
|
||||
isText() -> {
|
||||
text = if (data == null) {
|
||||
from.getString(from.getColumnIndexOrThrow("text"))
|
||||
} else {
|
||||
val uri = ContentUris.withAppendedId(CONTENT_URI, id)
|
||||
val sb = StringBuilder()
|
||||
context.contentResolver.openInputStream(uri)?.use { inputStream ->
|
||||
val isr = InputStreamReader(inputStream, "UTF-8")
|
||||
val reader = BufferedReader(isr)
|
||||
var temp = reader.readLine()
|
||||
while (temp != null) {
|
||||
sb.append(temp)
|
||||
temp = reader.readLine()
|
||||
}
|
||||
}
|
||||
sb.toString()
|
||||
}
|
||||
}
|
||||
|
||||
else -> Timber.v("Unhandled type: $type")
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPartsCursor(messageId: Long): Cursor? {
|
||||
return context.contentResolver.query(CONTENT_URI, null,
|
||||
"${Telephony.Mms.Part.MSG_ID} = ?", arrayOf(messageId.toString()), null)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.moez.QKSMS.mapper
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import com.moez.QKSMS.manager.PermissionManager
|
||||
import com.moez.QKSMS.model.Recipient
|
||||
import javax.inject.Inject
|
||||
|
||||
class CursorToRecipientImpl @Inject constructor(
|
||||
private val context: Context,
|
||||
private val permissionManager: PermissionManager
|
||||
) : CursorToRecipient {
|
||||
|
||||
companion object {
|
||||
val URI = Uri.parse("content://mms-sms/canonical-addresses")
|
||||
|
||||
const val COLUMN_ID = 0
|
||||
const val COLUMN_ADDRESS = 1
|
||||
}
|
||||
|
||||
override fun map(from: Cursor) = Recipient().apply {
|
||||
id = from.getLong(COLUMN_ID)
|
||||
address = from.getString(COLUMN_ADDRESS)
|
||||
lastUpdate = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
override fun getRecipientCursor(): Cursor? {
|
||||
return when (permissionManager.hasSms()) {
|
||||
true -> context.contentResolver.query(URI, null, null, null, null)
|
||||
false -> null
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRecipientCursor(id: Long): Cursor? {
|
||||
return context.contentResolver.query(URI, null, "_id = ?", arrayOf(id.toString()), null)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.moez.QKSMS.mapper
|
||||
|
||||
import com.f2prateek.rx.preferences2.RxSharedPreferences
|
||||
import com.moez.QKSMS.manager.AnalyticsManager
|
||||
import com.moez.QKSMS.manager.RatingManager
|
||||
import io.reactivex.rxkotlin.Observables
|
||||
import javax.inject.Inject
|
||||
|
||||
class RatingManagerImpl @Inject constructor(
|
||||
rxPrefs: RxSharedPreferences,
|
||||
private val analyticsManager: AnalyticsManager
|
||||
) : RatingManager {
|
||||
|
||||
companion object {
|
||||
private const val SESSION_THRESHOLD = 100
|
||||
}
|
||||
|
||||
private val sessions = rxPrefs.getInteger("sessions", 0)
|
||||
private val rated = rxPrefs.getBoolean("rated", false)
|
||||
private val dismissed = rxPrefs.getBoolean("dismissed", false)
|
||||
|
||||
override val shouldShowRating = Observables.combineLatest(
|
||||
sessions.asObservable(),
|
||||
rated.asObservable(),
|
||||
dismissed.asObservable(), { sessions, rated, dismissed ->
|
||||
|
||||
sessions > SESSION_THRESHOLD && !rated && !dismissed
|
||||
})
|
||||
|
||||
override fun addSession() {
|
||||
sessions.set(sessions.get() + 1)
|
||||
}
|
||||
|
||||
override fun rate() {
|
||||
analyticsManager.track("Clicked Rate")
|
||||
rated.set(true)
|
||||
}
|
||||
|
||||
override fun dismiss() {
|
||||
analyticsManager.track("Clicked Rate (Dismiss)")
|
||||
dismissed.set(true)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.moez.QKSMS.migration
|
||||
|
||||
import io.realm.DynamicRealm
|
||||
import io.realm.FieldAttribute
|
||||
import io.realm.RealmMigration
|
||||
|
||||
|
||||
class QkRealmMigration : RealmMigration {
|
||||
|
||||
companion object {
|
||||
const val SCHEMA_VERSION: Long = 3
|
||||
}
|
||||
|
||||
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
|
||||
var version = oldVersion
|
||||
|
||||
if (version == 0L) {
|
||||
realm.schema.get("MmsPart")
|
||||
?.removeField("image")
|
||||
|
||||
version++
|
||||
}
|
||||
|
||||
if (version == 1L) {
|
||||
realm.schema.get("Message")
|
||||
?.addField("subId", Int::class.java)
|
||||
|
||||
version++
|
||||
}
|
||||
|
||||
if (version == 2L) {
|
||||
realm.schema.get("Conversation")
|
||||
?.addField("name", String::class.java, FieldAttribute.REQUIRED)
|
||||
|
||||
version++
|
||||
}
|
||||
|
||||
if (version < newVersion) {
|
||||
throw IllegalStateException("Migration missing from v$oldVersion to v$newVersion")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.moez.QKSMS.receiver
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.provider.Telephony
|
||||
import android.support.annotation.RequiresApi
|
||||
import com.moez.QKSMS.interactor.SyncMessages
|
||||
import com.moez.QKSMS.util.Preferences
|
||||
import dagger.android.AndroidInjection
|
||||
import javax.inject.Inject
|
||||
|
||||
class DefaultSmsChangedReceiver : BroadcastReceiver() {
|
||||
|
||||
@Inject lateinit var prefs: Preferences
|
||||
@Inject lateinit var syncMessages: SyncMessages
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
AndroidInjection.inject(this, context)
|
||||
|
||||
if (intent.getBooleanExtra(Telephony.Sms.Intents.EXTRA_IS_DEFAULT_SMS_APP, false)) {
|
||||
val pendingResult = goAsync()
|
||||
syncMessages.execute(Unit, { pendingResult.finish() })
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.moez.QKSMS.receiver
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.moez.QKSMS.interactor.MarkRead
|
||||
import dagger.android.AndroidInjection
|
||||
import javax.inject.Inject
|
||||
|
||||
class MarkReadReceiver : BroadcastReceiver() {
|
||||
|
||||
@Inject lateinit var markRead: MarkRead
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
AndroidInjection.inject(this, context)
|
||||
|
||||
val threadId = intent.getLongExtra("threadId", 0)
|
||||
markRead.execute(listOf(threadId))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.moez.QKSMS.receiver
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.moez.QKSMS.interactor.MarkSeen
|
||||
import dagger.android.AndroidInjection
|
||||
import javax.inject.Inject
|
||||
|
||||
class MarkSeenReceiver : BroadcastReceiver() {
|
||||
|
||||
@Inject lateinit var markSeen: MarkSeen
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
AndroidInjection.inject(this, context)
|
||||
|
||||
val threadId = intent.getLongExtra("threadId", 0)
|
||||
markSeen.execute(threadId)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.moez.QKSMS.receiver
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import com.klinker.android.send_message.MmsReceivedReceiver
|
||||
import com.moez.QKSMS.interactor.ReceiveMms
|
||||
import dagger.android.AndroidInjection
|
||||
import javax.inject.Inject
|
||||
|
||||
class MmsReceivedReceiver : MmsReceivedReceiver() {
|
||||
|
||||
@Inject lateinit var receiveMms: ReceiveMms
|
||||
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
AndroidInjection.inject(this, context)
|
||||
super.onReceive(context, intent)
|
||||
}
|
||||
|
||||
override fun onMessageReceived(messageUri: Uri?) {
|
||||
messageUri?.let { uri ->
|
||||
val pendingResult = goAsync()
|
||||
receiveMms.execute(uri) { pendingResult.finish() }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.moez.QKSMS.receiver
|
||||
|
||||
import com.android.mms.transaction.PushReceiver
|
||||
|
||||
class MmsReceiver : PushReceiver()
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.moez.QKSMS.receiver
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.ContentUris
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.provider.Telephony
|
||||
import com.google.android.mms.MmsException
|
||||
import com.google.android.mms.util_alt.SqliteWrapper
|
||||
import com.klinker.android.send_message.Transaction
|
||||
import com.moez.QKSMS.interactor.SyncMessage
|
||||
import dagger.android.AndroidInjection
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
class MmsSentReceiver : BroadcastReceiver() {
|
||||
|
||||
@Inject lateinit var syncMessage: SyncMessage
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
AndroidInjection.inject(this, context)
|
||||
|
||||
Timber.v("MMS sending result: $resultCode")
|
||||
val uri = Uri.parse(intent.getStringExtra(Transaction.EXTRA_CONTENT_URI))
|
||||
Timber.v(uri.toString())
|
||||
|
||||
when (resultCode) {
|
||||
Activity.RESULT_OK -> {
|
||||
Timber.v("MMS has finished sending, marking it as so in the database")
|
||||
val values = ContentValues(1)
|
||||
values.put(Telephony.Mms.MESSAGE_BOX, Telephony.Mms.MESSAGE_BOX_SENT)
|
||||
SqliteWrapper.update(context, context.contentResolver, uri, values, null, null)
|
||||
}
|
||||
|
||||
else -> {
|
||||
Timber.v("MMS has failed to send, marking it as so in the database")
|
||||
try {
|
||||
val messageId = ContentUris.parseId(uri)
|
||||
|
||||
val values = ContentValues(1)
|
||||
values.put(Telephony.Mms.MESSAGE_BOX, Telephony.Mms.MESSAGE_BOX_FAILED)
|
||||
SqliteWrapper.update(context, context.contentResolver, Telephony.Mms.CONTENT_URI, values,
|
||||
"${Telephony.Mms._ID} = ?", arrayOf(messageId.toString()))
|
||||
|
||||
// TODO this query isn't able to find any results
|
||||
// Need to figure out why the message isn't appearing in the PendingMessages Uri,
|
||||
// so that we can properly assign the error type
|
||||
val errorTypeValues = ContentValues(1)
|
||||
errorTypeValues.put(Telephony.MmsSms.PendingMessages.ERROR_TYPE, Telephony.MmsSms.ERR_TYPE_GENERIC_PERMANENT)
|
||||
SqliteWrapper.update(context, context.contentResolver, Telephony.MmsSms.PendingMessages.CONTENT_URI,
|
||||
errorTypeValues, "${Telephony.MmsSms.PendingMessages.MSG_ID} = ?", arrayOf(messageId.toString()))
|
||||
|
||||
} catch (e: MmsException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val filePath = intent.getStringExtra(Transaction.EXTRA_FILE_PATH)
|
||||
Timber.v(filePath)
|
||||
File(filePath).delete()
|
||||
|
||||
Uri.parse(intent.getStringExtra("content_uri"))?.let { uri ->
|
||||
val pendingResult = goAsync()
|
||||
syncMessage.execute(uri) { pendingResult.finish() }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.moez.QKSMS.receiver
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import com.moez.QKSMS.interactor.SyncMessage
|
||||
import dagger.android.AndroidInjection
|
||||
import javax.inject.Inject
|
||||
|
||||
class MmsUpdatedReceiver : BroadcastReceiver() {
|
||||
|
||||
companion object {
|
||||
const val URI = "uri"
|
||||
}
|
||||
|
||||
@Inject lateinit var syncMessage: SyncMessage
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
AndroidInjection.inject(this, context)
|
||||
|
||||
intent.getStringExtra(URI)?.let { uriString ->
|
||||
val pendingResult = goAsync()
|
||||
syncMessage.execute(Uri.parse(uriString)) { pendingResult.finish() }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.moez.QKSMS.receiver
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.moez.QKSMS.util.NightModeManager
|
||||
import dagger.android.AndroidInjection
|
||||
import javax.inject.Inject
|
||||
|
||||
class NightModeReceiver : BroadcastReceiver() {
|
||||
|
||||
@Inject lateinit var nightModeManager: NightModeManager
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
AndroidInjection.inject(this, context)
|
||||
|
||||
nightModeManager.updateCurrentTheme()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.moez.QKSMS.receiver
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.support.v4.app.RemoteInput
|
||||
import com.moez.QKSMS.compat.SubscriptionManagerCompat
|
||||
import com.moez.QKSMS.interactor.MarkRead
|
||||
import com.moez.QKSMS.interactor.SendMessage
|
||||
import com.moez.QKSMS.repository.ConversationRepository
|
||||
import com.moez.QKSMS.repository.MessageRepository
|
||||
import dagger.android.AndroidInjection
|
||||
import javax.inject.Inject
|
||||
|
||||
class RemoteMessagingReceiver : BroadcastReceiver() {
|
||||
|
||||
@Inject lateinit var conversationRepo: ConversationRepository
|
||||
@Inject lateinit var markRead: MarkRead
|
||||
@Inject lateinit var messageRepo: MessageRepository
|
||||
@Inject lateinit var sendMessage: SendMessage
|
||||
@Inject lateinit var subscriptionManager: SubscriptionManagerCompat
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
AndroidInjection.inject(this, context)
|
||||
|
||||
val remoteInput = RemoteInput.getResultsFromIntent(intent) ?: return
|
||||
val bundle = intent.extras ?: return
|
||||
|
||||
val threadId = bundle.getLong("threadId")
|
||||
val body = remoteInput.getCharSequence("body").toString()
|
||||
markRead.execute(listOf(threadId))
|
||||
|
||||
val lastMessage = messageRepo.getMessages(threadId).lastOrNull()
|
||||
val subId = subscriptionManager.activeSubscriptionInfoList.firstOrNull { it.subscriptionId == lastMessage?.subId }?.subscriptionId ?: -1
|
||||
val addresses = conversationRepo.getConversation(threadId)?.recipients?.map { it.address } ?: return
|
||||
|
||||
val pendingRepository = goAsync()
|
||||
sendMessage.execute(SendMessage.Params(subId, threadId, addresses, body)) { pendingRepository.finish() }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.moez.QKSMS.receiver
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.moez.QKSMS.interactor.RetrySending
|
||||
import com.moez.QKSMS.repository.MessageRepository
|
||||
import dagger.android.AndroidInjection
|
||||
import javax.inject.Inject
|
||||
|
||||
class SendSmsReceiver : BroadcastReceiver() {
|
||||
|
||||
@Inject lateinit var messageRepo: MessageRepository
|
||||
@Inject lateinit var retrySending: RetrySending
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
AndroidInjection.inject(this, context)
|
||||
|
||||
val id = intent.getLongExtra("id", 0L)
|
||||
messageRepo.getMessage(id)?.let { message ->
|
||||
val result = goAsync()
|
||||
retrySending.execute(message) { result.finish() }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.moez.QKSMS.receiver
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.moez.QKSMS.interactor.MarkDelivered
|
||||
import com.moez.QKSMS.interactor.MarkDeliveryFailed
|
||||
import dagger.android.AndroidInjection
|
||||
import javax.inject.Inject
|
||||
|
||||
class SmsDeliveredReceiver : BroadcastReceiver() {
|
||||
|
||||
@Inject lateinit var markDelivered: MarkDelivered
|
||||
@Inject lateinit var markDeliveryFailed: MarkDeliveryFailed
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
AndroidInjection.inject(this, context)
|
||||
|
||||
val id = intent.getLongExtra("id", 0L)
|
||||
|
||||
when (resultCode) {
|
||||
// TODO notify about delivery
|
||||
Activity.RESULT_OK -> {
|
||||
val pendingResult = goAsync()
|
||||
markDelivered.execute(id) { pendingResult.finish() }
|
||||
}
|
||||
|
||||
// TODO notify about delivery failure
|
||||
Activity.RESULT_CANCELED -> {
|
||||
val pendingResult = goAsync()
|
||||
markDeliveryFailed.execute(MarkDeliveryFailed.Params(id, resultCode)) { pendingResult.finish() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.moez.QKSMS.receiver
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.moez.QKSMS.interactor.SyncMessage
|
||||
import dagger.android.AndroidInjection
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* When the SMS contentprovider is changed by a process other than us, we need to sync the Uri that
|
||||
* was changed.
|
||||
*
|
||||
* This can happen if a message is sent through something like Pushbullet or Google Assistant.
|
||||
*
|
||||
* This only works on API 24+, so to fully solve this problem we'll need a smarter way of running
|
||||
* partial syncs on older devices.
|
||||
*
|
||||
* https://developer.android.com/reference/android/provider/Telephony.Sms.Intents.html#ACTION_EXTERNAL_PROVIDER_CHANGE
|
||||
*/
|
||||
class SmsProviderChangedReceiver : BroadcastReceiver() {
|
||||
|
||||
@Inject lateinit var syncMessage: SyncMessage
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
AndroidInjection.inject(this, context)
|
||||
|
||||
// Obtain the uri for the changed data
|
||||
// If the value is null, don't continue
|
||||
val uri = intent.data ?: return
|
||||
|
||||
// Sync the message to our realm
|
||||
val pendingResult = goAsync()
|
||||
syncMessage.execute(uri) { pendingResult.finish() }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.moez.QKSMS.receiver
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.provider.Telephony.Sms
|
||||
import com.moez.QKSMS.interactor.ReceiveSms
|
||||
import dagger.android.AndroidInjection
|
||||
import javax.inject.Inject
|
||||
|
||||
class SmsReceiver : BroadcastReceiver() {
|
||||
|
||||
@Inject lateinit var receiveMessage: ReceiveSms
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
AndroidInjection.inject(this, context)
|
||||
|
||||
Sms.Intents.getMessagesFromIntent(intent)?.let { messages ->
|
||||
val subId = intent.extras?.getInt("subscription", -1) ?: -1
|
||||
|
||||
val pendingResult = goAsync()
|
||||
receiveMessage.execute(ReceiveSms.Params(subId, messages), { pendingResult.finish() })
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.moez.QKSMS.receiver
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.telephony.SmsManager
|
||||
import com.moez.QKSMS.interactor.MarkFailed
|
||||
import com.moez.QKSMS.interactor.MarkSent
|
||||
import dagger.android.AndroidInjection
|
||||
import javax.inject.Inject
|
||||
|
||||
class SmsSentReceiver : BroadcastReceiver() {
|
||||
|
||||
@Inject lateinit var markSent: MarkSent
|
||||
@Inject lateinit var markFailed: MarkFailed
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
AndroidInjection.inject(this, context)
|
||||
|
||||
val id = intent.getLongExtra("id", 0L)
|
||||
|
||||
when (resultCode) {
|
||||
Activity.RESULT_OK -> {
|
||||
val pendingResult = goAsync()
|
||||
markSent.execute(id) { pendingResult.finish() }
|
||||
}
|
||||
|
||||
SmsManager.RESULT_ERROR_GENERIC_FAILURE,
|
||||
SmsManager.RESULT_ERROR_NO_SERVICE,
|
||||
SmsManager.RESULT_ERROR_NULL_PDU,
|
||||
SmsManager.RESULT_ERROR_RADIO_OFF -> {
|
||||
val pendingResult = goAsync()
|
||||
markFailed.execute(MarkFailed.Params(id, resultCode)) { pendingResult.finish() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.moez.QKSMS.repository
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.provider.BaseColumns
|
||||
import android.provider.ContactsContract
|
||||
import com.moez.QKSMS.extensions.asFlowable
|
||||
import com.moez.QKSMS.extensions.mapNotNull
|
||||
import com.moez.QKSMS.model.Contact
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import io.realm.Realm
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class ContactRepositoryImpl @Inject constructor(val context: Context) : ContactRepository {
|
||||
|
||||
override fun findContactUri(address: String): Single<Uri> {
|
||||
return Flowable.just(address)
|
||||
.map {
|
||||
when {
|
||||
address.contains('@') -> Uri.withAppendedPath(ContactsContract.CommonDataKinds.Email.CONTENT_FILTER_URI, Uri.encode(address))
|
||||
else -> Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI, Uri.encode(address))
|
||||
}
|
||||
}
|
||||
.mapNotNull { uri -> context.contentResolver.query(uri, arrayOf(BaseColumns._ID), null, null, null) }
|
||||
.flatMap { cursor -> cursor.asFlowable() }
|
||||
.firstOrError()
|
||||
.map { cursor -> cursor.getString(cursor.getColumnIndexOrThrow(BaseColumns._ID)) }
|
||||
.map { id -> Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_URI, id) }
|
||||
}
|
||||
|
||||
override fun getUnmanagedContacts(): Flowable<List<Contact>> {
|
||||
val realm = Realm.getDefaultInstance()
|
||||
return realm.where(Contact::class.java)
|
||||
.sort("name")
|
||||
.findAllAsync()
|
||||
.asFlowable()
|
||||
.filter { it.isLoaded }
|
||||
.filter { it.isValid }
|
||||
.map { realm.copyFromRealm(it) }
|
||||
.subscribeOn(AndroidSchedulers.mainThread())
|
||||
.observeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
package com.moez.QKSMS.repository
|
||||
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.provider.Telephony
|
||||
import android.telephony.PhoneNumberUtils
|
||||
import com.moez.QKSMS.compat.TelephonyCompat
|
||||
import com.moez.QKSMS.extensions.anyOf
|
||||
import com.moez.QKSMS.extensions.map
|
||||
import com.moez.QKSMS.filter.ConversationFilter
|
||||
import com.moez.QKSMS.mapper.CursorToConversation
|
||||
import com.moez.QKSMS.mapper.CursorToRecipient
|
||||
import com.moez.QKSMS.model.Contact
|
||||
import com.moez.QKSMS.model.Conversation
|
||||
import com.moez.QKSMS.model.Message
|
||||
import com.moez.QKSMS.model.SearchResult
|
||||
import com.moez.QKSMS.util.tryOrNull
|
||||
import io.realm.Case
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmResults
|
||||
import io.realm.Sort
|
||||
import javax.inject.Inject
|
||||
|
||||
class ConversationRepositoryImpl @Inject constructor(
|
||||
private val context: Context,
|
||||
private val conversationFilter: ConversationFilter,
|
||||
private val cursorToConversation: CursorToConversation,
|
||||
private val cursorToRecipient: CursorToRecipient
|
||||
) : ConversationRepository {
|
||||
|
||||
override fun getConversations(archived: Boolean): RealmResults<Conversation> {
|
||||
return Realm.getDefaultInstance()
|
||||
.where(Conversation::class.java)
|
||||
.notEqualTo("id", 0L)
|
||||
.greaterThan("count", 0)
|
||||
.equalTo("archived", archived)
|
||||
.equalTo("blocked", false)
|
||||
.isNotEmpty("recipients")
|
||||
.sort("date", Sort.DESCENDING)
|
||||
.findAllAsync()
|
||||
}
|
||||
|
||||
override fun getConversationsSnapshot(): List<Conversation> {
|
||||
val realm = Realm.getDefaultInstance()
|
||||
return realm.copyFromRealm(realm.where(Conversation::class.java)
|
||||
.notEqualTo("id", 0L)
|
||||
.greaterThan("count", 0)
|
||||
.equalTo("archived", false)
|
||||
.equalTo("blocked", false)
|
||||
.isNotEmpty("recipients")
|
||||
.sort("date", Sort.DESCENDING)
|
||||
.findAll())
|
||||
}
|
||||
|
||||
override fun setConversationName(id: Long, name: String) {
|
||||
Realm.getDefaultInstance().use { realm ->
|
||||
realm.executeTransaction {
|
||||
realm.where(Conversation::class.java)
|
||||
.equalTo("id", id)
|
||||
.findFirst()
|
||||
?.name = name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchConversations(query: String): List<SearchResult> {
|
||||
val conversations = getConversationsSnapshot()
|
||||
|
||||
val messagesByConversation = Realm.getDefaultInstance()
|
||||
.where(Message::class.java)
|
||||
.contains("body", query, Case.INSENSITIVE)
|
||||
.findAll()
|
||||
.groupBy { message -> message.threadId }
|
||||
.filter { (threadId, _) -> conversations.firstOrNull { it.id == threadId } != null }
|
||||
.map { (threadId, messages) -> Pair(conversations.first { it.id == threadId }, messages.size) }
|
||||
.map { (conversation, messages) -> SearchResult(query, conversation, messages) }
|
||||
.sortedByDescending { result -> result.messages }
|
||||
|
||||
return conversations
|
||||
.filter { conversation -> conversationFilter.filter(conversation, query) }
|
||||
.map { conversation -> SearchResult(query, conversation, 0) }
|
||||
.plus(messagesByConversation)
|
||||
}
|
||||
|
||||
override fun getBlockedConversations(): RealmResults<Conversation> {
|
||||
return Realm.getDefaultInstance()
|
||||
.where(Conversation::class.java)
|
||||
.equalTo("blocked", true)
|
||||
.findAllAsync()
|
||||
}
|
||||
|
||||
override fun getConversationAsync(threadId: Long): Conversation {
|
||||
return Realm.getDefaultInstance()
|
||||
.where(Conversation::class.java)
|
||||
.equalTo("id", threadId)
|
||||
.findFirstAsync()
|
||||
}
|
||||
|
||||
override fun getConversation(threadId: Long): Conversation? {
|
||||
return Realm.getDefaultInstance()
|
||||
.where(Conversation::class.java)
|
||||
.equalTo("id", threadId)
|
||||
.findFirst()
|
||||
}
|
||||
|
||||
override fun getThreadId(recipient: String): Long? {
|
||||
return getThreadId(listOf(recipient))
|
||||
}
|
||||
|
||||
override fun getThreadId(recipients: Collection<String>): Long? {
|
||||
return Realm.getDefaultInstance().use { realm ->
|
||||
realm.where(Conversation::class.java)
|
||||
.findAll()
|
||||
.firstOrNull { conversation ->
|
||||
conversation.recipients.size == recipients.size && conversation.recipients.map { it.address }.all { address ->
|
||||
recipients.any { recipient -> PhoneNumberUtils.compare(recipient, address) }
|
||||
}
|
||||
}
|
||||
?.id
|
||||
}
|
||||
}
|
||||
|
||||
override fun getOrCreateConversation(threadId: Long): Conversation? {
|
||||
return getConversation(threadId) ?: getConversationFromCp(threadId)
|
||||
}
|
||||
|
||||
override fun getOrCreateConversation(address: String): Conversation? {
|
||||
return getOrCreateConversation(listOf(address))
|
||||
}
|
||||
|
||||
override fun getOrCreateConversation(addresses: List<String>): Conversation? {
|
||||
return tryOrNull { TelephonyCompat.getOrCreateThreadId(context, addresses.toSet()) }
|
||||
?.takeIf { threadId -> threadId != 0L }
|
||||
?.let { threadId ->
|
||||
var conversation = getConversation(threadId)
|
||||
if (conversation != null) conversation = Realm.getDefaultInstance().copyFromRealm(conversation)
|
||||
|
||||
conversation ?: getConversationFromCp(threadId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun saveDraft(threadId: Long, draft: String) {
|
||||
Realm.getDefaultInstance().use { realm ->
|
||||
val conversation = realm.where(Conversation::class.java)
|
||||
.equalTo("id", threadId)
|
||||
.findFirst()
|
||||
|
||||
conversation?.let {
|
||||
realm.executeTransaction {
|
||||
conversation.draft = draft
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateConversations(vararg threadIds: Long) {
|
||||
Realm.getDefaultInstance().use { realm ->
|
||||
realm.refresh()
|
||||
|
||||
threadIds.forEach { threadId ->
|
||||
val conversation = realm
|
||||
.where(Conversation::class.java)
|
||||
.equalTo("id", threadId)
|
||||
.findFirst() ?: return
|
||||
|
||||
val messages = realm
|
||||
.where(Message::class.java)
|
||||
.equalTo("threadId", threadId)
|
||||
.sort("date", Sort.DESCENDING)
|
||||
.findAll()
|
||||
|
||||
val message = messages.firstOrNull()
|
||||
|
||||
realm.executeTransaction {
|
||||
conversation.count = messages.size
|
||||
conversation.date = message?.date ?: 0
|
||||
conversation.snippet = message?.getSummary() ?: ""
|
||||
conversation.read = message?.read ?: true
|
||||
conversation.me = message?.isMe() ?: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun markArchived(vararg threadIds: Long) {
|
||||
Realm.getDefaultInstance().use { realm ->
|
||||
val conversations = realm.where(Conversation::class.java)
|
||||
.anyOf("id", threadIds)
|
||||
.findAll()
|
||||
|
||||
realm.executeTransaction {
|
||||
conversations.forEach { it.archived = true }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun markUnarchived(vararg threadIds: Long) {
|
||||
Realm.getDefaultInstance().use { realm ->
|
||||
val conversations = realm.where(Conversation::class.java)
|
||||
.anyOf("id", threadIds)
|
||||
.findAll()
|
||||
|
||||
realm.executeTransaction {
|
||||
conversations.forEach { it.archived = false }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun markBlocked(vararg threadIds: Long) {
|
||||
Realm.getDefaultInstance().use { realm ->
|
||||
val conversations = realm.where(Conversation::class.java)
|
||||
.anyOf("id", threadIds)
|
||||
.findAll()
|
||||
|
||||
realm.executeTransaction {
|
||||
conversations.forEach { it.blocked = true }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun markUnblocked(vararg threadIds: Long) {
|
||||
Realm.getDefaultInstance().use { realm ->
|
||||
val conversations = realm.where(Conversation::class.java)
|
||||
.anyOf("id", threadIds)
|
||||
.equalTo("archived", false)
|
||||
.findAll()
|
||||
|
||||
realm.executeTransaction {
|
||||
conversations.forEach { it.blocked = false }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun deleteConversations(vararg threadIds: Long) {
|
||||
Realm.getDefaultInstance().use { realm ->
|
||||
val conversation = realm.where(Conversation::class.java).anyOf("id", threadIds).findAll()
|
||||
val messages = realm.where(Message::class.java).anyOf("threadId", threadIds).findAll()
|
||||
|
||||
realm.executeTransaction {
|
||||
conversation.deleteAllFromRealm()
|
||||
messages.deleteAllFromRealm()
|
||||
}
|
||||
}
|
||||
|
||||
threadIds.forEach { threadId ->
|
||||
val uri = ContentUris.withAppendedId(Telephony.Threads.CONTENT_URI, threadId)
|
||||
context.contentResolver.delete(uri, null, null)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a [Conversation] from the system SMS ContentProvider, based on the [threadId]
|
||||
*
|
||||
* It should be noted that even if we have a valid [threadId], that does not guarantee that
|
||||
* we can return a [Conversation]. On some devices, the ContentProvider won't return the
|
||||
* conversation unless it contains at least 1 message
|
||||
*/
|
||||
private fun getConversationFromCp(threadId: Long): Conversation? {
|
||||
return cursorToConversation.getConversationCursor(threadId)
|
||||
?.takeIf { cursor -> cursor.moveToFirst() }
|
||||
?.use { cursor ->
|
||||
val conversation = cursorToConversation.map(cursor)
|
||||
|
||||
val realm = Realm.getDefaultInstance()
|
||||
val contacts = realm.copyFromRealm(realm.where(Contact::class.java).findAll())
|
||||
|
||||
val recipients = conversation.recipients
|
||||
.map { recipient -> recipient.id }
|
||||
.map { id -> cursorToRecipient.getRecipientCursor(id) }
|
||||
.mapNotNull { recipientCursor ->
|
||||
// Map the recipient cursor to a list of recipients
|
||||
recipientCursor?.use { recipientCursor.map { cursorToRecipient.map(recipientCursor) } }
|
||||
}
|
||||
.flatten()
|
||||
.map { recipient ->
|
||||
recipient.apply {
|
||||
contact = contacts.firstOrNull {
|
||||
it.numbers.any { PhoneNumberUtils.compare(recipient.address, it.address) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
conversation.recipients.clear()
|
||||
conversation.recipients.addAll(recipients)
|
||||
realm.executeTransaction { it.insertOrUpdate(conversation) }
|
||||
realm.close()
|
||||
|
||||
conversation
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package com.moez.QKSMS.repository
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.ContentUris
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Matrix
|
||||
import android.net.Uri
|
||||
import android.provider.MediaStore
|
||||
import android.provider.MediaStore.Images
|
||||
import android.support.media.ExifInterface
|
||||
import com.moez.QKSMS.util.tryOrNull
|
||||
import javax.inject.Inject
|
||||
|
||||
class ImageRepostoryImpl @Inject constructor(private val context: Context) : ImageRepository {
|
||||
|
||||
override fun loadImage(uri: Uri): Bitmap? {
|
||||
val exif = ExifInterface(context.contentResolver.openInputStream(uri))
|
||||
val bitmap = BitmapFactory.decodeStream(context.contentResolver.openInputStream(uri))
|
||||
val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
|
||||
|
||||
|
||||
return when (orientation) {
|
||||
ExifInterface.ORIENTATION_ROTATE_90 -> rotateBitmap(bitmap, 90f)
|
||||
ExifInterface.ORIENTATION_ROTATE_180 -> rotateBitmap(bitmap, 180f)
|
||||
ExifInterface.ORIENTATION_ROTATE_270 -> rotateBitmap(bitmap, 270f)
|
||||
else -> bitmap
|
||||
}
|
||||
}
|
||||
|
||||
private fun rotateBitmap(bitmap: Bitmap, degree: Float): Bitmap {
|
||||
val w = bitmap.width
|
||||
val h = bitmap.height
|
||||
|
||||
val mtx = Matrix()
|
||||
mtx.postRotate(degree)
|
||||
|
||||
return Bitmap.createBitmap(bitmap, 0, 0, w, h, mtx, true)
|
||||
}
|
||||
|
||||
override fun saveImage(uri: Uri) {
|
||||
val bitmap = tryOrNull { MediaStore.Images.Media.getBitmap(context.contentResolver, uri) }
|
||||
|
||||
var title = ""
|
||||
var description = ""
|
||||
|
||||
when (uri.scheme) {
|
||||
"file" -> title = uri.lastPathSegment
|
||||
|
||||
"content" -> {
|
||||
val projection = arrayOf(Images.Media.TITLE, Images.Media.DESCRIPTION)
|
||||
context.contentResolver.query(uri, projection, null, null, null)?.use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
title = cursor.getString(0)
|
||||
description = cursor.getString(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
insertImage(context.contentResolver, bitmap, title, description)
|
||||
}
|
||||
|
||||
/**
|
||||
* Modified from [android.provider.MediaStore.Images.Media.insertImage] to add date fields so
|
||||
* that the saved image is put at the start of the gallery
|
||||
*/
|
||||
private fun insertImage(cr: ContentResolver, source: Bitmap?, title: String, description: String): String? {
|
||||
|
||||
val values = ContentValues()
|
||||
values.put(Images.Media.TITLE, title)
|
||||
values.put(Images.Media.DISPLAY_NAME, title)
|
||||
values.put(Images.Media.DESCRIPTION, description)
|
||||
values.put(Images.Media.MIME_TYPE, "image/jpeg")
|
||||
|
||||
// Add the date meta data to ensure the image is added at the front of the gallery
|
||||
values.put(Images.Media.DATE_ADDED, System.currentTimeMillis() / 1000)
|
||||
values.put(Images.Media.DATE_TAKEN, System.currentTimeMillis())
|
||||
|
||||
var uri: Uri? = null
|
||||
|
||||
try {
|
||||
uri = cr.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
|
||||
|
||||
if (source != null) {
|
||||
cr.openOutputStream(uri).use { imageOut ->
|
||||
source.compress(Bitmap.CompressFormat.JPEG, 100, imageOut)
|
||||
}
|
||||
|
||||
val id = ContentUris.parseId(uri)
|
||||
// Wait until MINI_KIND thumbnail is generated.
|
||||
val miniThumb = Images.Thumbnails.getThumbnail(cr, id, Images.Thumbnails.MINI_KIND, null)
|
||||
// This is for backward compatibility.
|
||||
storeThumbnail(cr, miniThumb, id, 50f, 50f, Images.Thumbnails.MICRO_KIND)
|
||||
} else {
|
||||
cr.delete(uri, null, null)
|
||||
uri = null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (uri != null) {
|
||||
cr.delete(uri, null, null)
|
||||
uri = null
|
||||
}
|
||||
}
|
||||
|
||||
return uri?.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Modified from [android.provider.MediaStore.Images.Media.insertImage] to add date fields so
|
||||
* that the saved image is put at the start of the gallery
|
||||
*/
|
||||
private fun storeThumbnail(cr: ContentResolver, source: Bitmap, id: Long, width: Float, height: Float, kind: Int): Bitmap? {
|
||||
val matrix = Matrix()
|
||||
|
||||
val scaleX = width / source.width
|
||||
val scaleY = height / source.height
|
||||
|
||||
matrix.setScale(scaleX, scaleY)
|
||||
|
||||
val thumb = Bitmap.createBitmap(source, 0, 0, source.width, source.height, matrix, true)
|
||||
|
||||
val values = ContentValues(4)
|
||||
values.put(Images.Thumbnails.KIND, kind)
|
||||
values.put(Images.Thumbnails.IMAGE_ID, id.toInt())
|
||||
values.put(Images.Thumbnails.HEIGHT, thumb.height)
|
||||
values.put(Images.Thumbnails.WIDTH, thumb.width)
|
||||
|
||||
val url = cr.insert(Images.Thumbnails.EXTERNAL_CONTENT_URI, values)
|
||||
|
||||
return tryOrNull {
|
||||
val thumbOut = cr.openOutputStream(url)
|
||||
thumb.compress(Bitmap.CompressFormat.JPEG, 100, thumbOut)
|
||||
thumbOut.close()
|
||||
thumb
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,521 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.moez.QKSMS.repository
|
||||
|
||||
import android.app.AlarmManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.ContentUris
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build
|
||||
import android.provider.Telephony
|
||||
import android.telephony.SmsManager
|
||||
import com.google.android.mms.ContentType
|
||||
import com.klinker.android.send_message.BroadcastUtils
|
||||
import com.klinker.android.send_message.Settings
|
||||
import com.klinker.android.send_message.StripAccents
|
||||
import com.klinker.android.send_message.Transaction
|
||||
import com.moez.QKSMS.extensions.anyOf
|
||||
import com.moez.QKSMS.extensions.insertOrUpdate
|
||||
import com.moez.QKSMS.manager.KeyManager
|
||||
import com.moez.QKSMS.model.Attachment
|
||||
import com.moez.QKSMS.model.Conversation
|
||||
import com.moez.QKSMS.model.Message
|
||||
import com.moez.QKSMS.model.MmsPart
|
||||
import com.moez.QKSMS.util.Preferences
|
||||
import io.realm.Case
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmResults
|
||||
import io.realm.Sort
|
||||
import timber.log.Timber
|
||||
import java.io.ByteArrayOutputStream
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class MessageRepositoryImpl @Inject constructor(
|
||||
private val context: Context,
|
||||
private val messageIds: KeyManager,
|
||||
private val conversationRepo: ConversationRepository,
|
||||
private val imageRepository: ImageRepository,
|
||||
private val prefs: Preferences) : MessageRepository {
|
||||
|
||||
override fun getMessages(threadId: Long, query: String): RealmResults<Message> {
|
||||
return Realm.getDefaultInstance()
|
||||
.where(Message::class.java)
|
||||
.equalTo("threadId", threadId)
|
||||
.let { if (query.isEmpty()) it else it.contains("body", query, Case.INSENSITIVE) }
|
||||
.sort("date")
|
||||
.findAllAsync()
|
||||
}
|
||||
|
||||
override fun getMessage(id: Long): Message? {
|
||||
return Realm.getDefaultInstance()
|
||||
.where(Message::class.java)
|
||||
.equalTo("id", id)
|
||||
.findFirst()
|
||||
}
|
||||
|
||||
override fun getMessageForPart(id: Long): Message? {
|
||||
return Realm.getDefaultInstance()
|
||||
.where(Message::class.java)
|
||||
.equalTo("parts.id", id)
|
||||
.findFirst()
|
||||
}
|
||||
|
||||
override fun getUnreadCount(): Long {
|
||||
return Realm.getDefaultInstance()
|
||||
.where(Conversation::class.java)
|
||||
.equalTo("archived", false)
|
||||
.equalTo("blocked", false)
|
||||
.equalTo("read", false)
|
||||
.count()
|
||||
}
|
||||
|
||||
override fun getPart(id: Long): MmsPart? {
|
||||
return Realm.getDefaultInstance()
|
||||
.where(MmsPart::class.java)
|
||||
.equalTo("id", id)
|
||||
.findFirst()
|
||||
}
|
||||
|
||||
override fun getPartsForConversation(threadId: Long): RealmResults<MmsPart> {
|
||||
return Realm.getDefaultInstance()
|
||||
.where(MmsPart::class.java)
|
||||
.equalTo("messages.threadId", threadId)
|
||||
.beginGroup()
|
||||
.contains("type", "image/")
|
||||
.or()
|
||||
.contains("type", "video/")
|
||||
.endGroup()
|
||||
.sort("id", Sort.DESCENDING)
|
||||
.findAllAsync()
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the list of messages which should be shown in the notification
|
||||
* for a given conversation
|
||||
*/
|
||||
override fun getUnreadUnseenMessages(threadId: Long): RealmResults<Message> {
|
||||
return Realm.getDefaultInstance()
|
||||
.also { it.refresh() }
|
||||
.where(Message::class.java)
|
||||
.equalTo("seen", false)
|
||||
.equalTo("read", false)
|
||||
.equalTo("threadId", threadId)
|
||||
.sort("date")
|
||||
.findAll()
|
||||
}
|
||||
|
||||
override fun getUnreadMessages(threadId: Long): RealmResults<Message> {
|
||||
return Realm.getDefaultInstance()
|
||||
.where(Message::class.java)
|
||||
.equalTo("read", false)
|
||||
.equalTo("threadId", threadId)
|
||||
.sort("date")
|
||||
.findAll()
|
||||
}
|
||||
|
||||
override fun markAllSeen() {
|
||||
val realm = Realm.getDefaultInstance()
|
||||
val messages = realm.where(Message::class.java).equalTo("seen", false).findAll()
|
||||
realm.executeTransaction { messages.forEach { message -> message.seen = true } }
|
||||
realm.close()
|
||||
}
|
||||
|
||||
override fun markSeen(threadId: Long) {
|
||||
val realm = Realm.getDefaultInstance()
|
||||
val messages = realm.where(Message::class.java)
|
||||
.equalTo("threadId", threadId)
|
||||
.equalTo("seen", false)
|
||||
.findAll()
|
||||
|
||||
realm.executeTransaction {
|
||||
messages.forEach { message ->
|
||||
message.seen = true
|
||||
}
|
||||
}
|
||||
realm.close()
|
||||
}
|
||||
|
||||
override fun markRead(vararg threadIds: Long) {
|
||||
Realm.getDefaultInstance()?.use { realm ->
|
||||
val messages = realm.where(Message::class.java)
|
||||
.anyOf("threadId", threadIds)
|
||||
.beginGroup()
|
||||
.equalTo("read", false)
|
||||
.or()
|
||||
.equalTo("seen", false)
|
||||
.endGroup()
|
||||
.findAll()
|
||||
|
||||
realm.executeTransaction {
|
||||
messages.forEach { message ->
|
||||
message.seen = true
|
||||
message.read = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val values = ContentValues()
|
||||
values.put(Telephony.Sms.SEEN, true)
|
||||
values.put(Telephony.Sms.READ, true)
|
||||
|
||||
threadIds.forEach { threadId ->
|
||||
try {
|
||||
val uri = ContentUris.withAppendedId(Telephony.MmsSms.CONTENT_CONVERSATIONS_URI, threadId)
|
||||
context.contentResolver.update(uri, values, "${Telephony.Sms.READ} = 0", null)
|
||||
} catch (exception: Exception) {
|
||||
Timber.w(exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun markUnread(vararg threadIds: Long) {
|
||||
Realm.getDefaultInstance()?.use { realm ->
|
||||
val conversation = realm.where(Conversation::class.java)
|
||||
.anyOf("id", threadIds)
|
||||
.equalTo("read", true)
|
||||
.findAll()
|
||||
|
||||
realm.executeTransaction {
|
||||
conversation.forEach { it.read = false }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun sendSmsAndPersist(subId: Int, threadId: Long, address: String, body: String) {
|
||||
if (prefs.sendDelay.get() != Preferences.SEND_DELAY_NONE) {
|
||||
val delay = when (prefs.sendDelay.get()) {
|
||||
Preferences.SEND_DELAY_SHORT -> 3000
|
||||
Preferences.SEND_DELAY_MEDIUM -> 5000
|
||||
Preferences.SEND_DELAY_LONG -> 10000
|
||||
else -> 0
|
||||
}
|
||||
|
||||
val sendTime = System.currentTimeMillis() + delay
|
||||
val message = insertSentSms(subId, threadId, address, body, sendTime)
|
||||
|
||||
val intent = getIntentForDelayedSms(message.id)
|
||||
|
||||
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, sendTime, intent)
|
||||
} else {
|
||||
alarmManager.setExact(AlarmManager.RTC_WAKEUP, sendTime, intent)
|
||||
}
|
||||
} else {
|
||||
val message = insertSentSms(subId, threadId, address, body, System.currentTimeMillis())
|
||||
sendSms(message)
|
||||
}
|
||||
}
|
||||
|
||||
override fun sendSms(message: Message) {
|
||||
val smsManager = message.subId.takeIf { it != -1 }
|
||||
?.let(SmsManager::getSmsManagerForSubscriptionId)
|
||||
?: SmsManager.getDefault()
|
||||
|
||||
val parts = smsManager.divideMessage(if (prefs.unicode.get()) StripAccents.stripAccents(message.body) else message.body)
|
||||
?: arrayListOf()
|
||||
|
||||
val sentIntents = parts.map {
|
||||
val action = "com.moez.QKSMS.SMS_SENT"
|
||||
val intent = Intent(action).putExtra("id", message.id)
|
||||
BroadcastUtils.addClassName(context, intent, action)
|
||||
PendingIntent.getBroadcast(context, message.id.toInt(), intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
||||
val deliveredIntents = parts.map {
|
||||
val action = "com.moez.QKSMS.SMS_DELIVERED"
|
||||
val intent = Intent(action).putExtra("id", message.id)
|
||||
BroadcastUtils.addClassName(context, intent, action)
|
||||
val pendingIntent = PendingIntent.getBroadcast(context, message.id.toInt(), intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
if (prefs.delivery.get()) pendingIntent else null
|
||||
}
|
||||
|
||||
smsManager.sendMultipartTextMessage(message.address, null, parts, ArrayList(sentIntents), ArrayList(deliveredIntents))
|
||||
}
|
||||
|
||||
override fun sendMms(subId: Int, threadId: Long, addresses: List<String>, body: String, attachments: List<Attachment>) {
|
||||
val settings = Settings().apply { subscriptionId = subId }
|
||||
val message = com.klinker.android.send_message.Message(body, addresses.toTypedArray())
|
||||
|
||||
// Add the GIFs as attachments. The app currently can't compress them, which may result
|
||||
// in a lot of these messages failing to send
|
||||
// TODO Add support for GIF compression
|
||||
attachments
|
||||
.filter { attachment -> attachment.isGif(context) }
|
||||
.map { attachment -> attachment.getUri() }
|
||||
.map { uri -> context.contentResolver.openInputStream(uri) }
|
||||
.map { inputStream -> inputStream.readBytes() }
|
||||
.forEach { bitmap -> message.addMedia(bitmap, ContentType.IMAGE_GIF) }
|
||||
|
||||
// Compress the images and add them as attachments
|
||||
var totalImageBytes = 0
|
||||
attachments
|
||||
.filter { attachment -> !attachment.isGif(context) }
|
||||
.mapNotNull { attachment -> attachment.getUri() }
|
||||
.mapNotNull { uri -> imageRepository.loadImage(uri) }
|
||||
.also { totalImageBytes = it.sumBy { it.allocationByteCount } }
|
||||
.map { bitmap ->
|
||||
val byteRatio = bitmap.allocationByteCount / totalImageBytes.toFloat()
|
||||
shrink(bitmap, (prefs.mmsSize.get() * 1024 * byteRatio).toInt())
|
||||
}
|
||||
.forEach { bitmap -> message.addMedia(bitmap, ContentType.IMAGE_JPEG) }
|
||||
|
||||
val transaction = Transaction(context, settings)
|
||||
transaction.sendNewMessage(message, threadId)
|
||||
}
|
||||
|
||||
private fun shrink(src: Bitmap, maxBytes: Int): ByteArray {
|
||||
var step = 0.0
|
||||
val factor = 0.5
|
||||
val quality = 60
|
||||
|
||||
val height = src.height
|
||||
val width = src.width
|
||||
|
||||
val stream = ByteArrayOutputStream()
|
||||
src.compress(Bitmap.CompressFormat.JPEG, 100, stream)
|
||||
|
||||
while (maxBytes > 0 && stream.size() > maxBytes) {
|
||||
step++
|
||||
val scale = Math.pow(factor, step)
|
||||
|
||||
stream.reset()
|
||||
Bitmap.createScaledBitmap(src, (width * scale).toInt(), (height * scale).toInt(), false)
|
||||
.compress(Bitmap.CompressFormat.JPEG, quality, stream)
|
||||
}
|
||||
|
||||
return stream.toByteArray()
|
||||
}
|
||||
|
||||
override fun cancelDelayedSms(id: Long) {
|
||||
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
alarmManager.cancel(getIntentForDelayedSms(id))
|
||||
}
|
||||
|
||||
private fun getIntentForDelayedSms(id: Long): PendingIntent {
|
||||
val action = "com.moez.QKSMS.SEND_SMS"
|
||||
val intent = Intent(action).putExtra("id", id)
|
||||
BroadcastUtils.addClassName(context, intent, action)
|
||||
return PendingIntent.getBroadcast(context, id.toInt(), intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
||||
override fun insertSentSms(subId: Int, threadId: Long, address: String, body: String, date: Long): Message {
|
||||
|
||||
// Insert the message to Realm
|
||||
val message = Message().apply {
|
||||
this.threadId = threadId
|
||||
this.address = address
|
||||
this.body = body
|
||||
this.date = date
|
||||
this.subId = subId
|
||||
|
||||
id = messageIds.newId()
|
||||
boxId = Telephony.Sms.MESSAGE_TYPE_OUTBOX
|
||||
type = "sms"
|
||||
read = true
|
||||
seen = true
|
||||
}
|
||||
val realm = Realm.getDefaultInstance()
|
||||
var managedMessage: Message? = null
|
||||
realm.executeTransaction { managedMessage = realm.copyToRealmOrUpdate(message) }
|
||||
|
||||
// Insert the message to the native content provider
|
||||
val values = ContentValues().apply {
|
||||
put(Telephony.Sms.ADDRESS, address)
|
||||
put(Telephony.Sms.BODY, body)
|
||||
put(Telephony.Sms.DATE, System.currentTimeMillis())
|
||||
put(Telephony.Sms.READ, true)
|
||||
put(Telephony.Sms.SEEN, true)
|
||||
put(Telephony.Sms.TYPE, Telephony.Sms.MESSAGE_TYPE_OUTBOX)
|
||||
put(Telephony.Sms.THREAD_ID, threadId)
|
||||
put(Telephony.Sms.SUBSCRIPTION_ID, subId)
|
||||
}
|
||||
val uri = context.contentResolver.insert(Telephony.Sms.CONTENT_URI, values)
|
||||
|
||||
// Update the contentId after the message has been inserted to the content provider
|
||||
// The message might have been deleted by now, so only proceed if it's valid
|
||||
//
|
||||
// We do this after inserting the message because it might be slow, and we want the message
|
||||
// to be inserted into Realm immediately. We don't need to do this after receiving one
|
||||
realm.executeTransaction { managedMessage?.takeIf { it.isValid }?.contentId = uri.lastPathSegment.toLong() }
|
||||
realm.close()
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
override fun insertReceivedSms(subId: Int, address: String, body: String, sentTime: Long): Message {
|
||||
|
||||
// Insert the message to the native content provider
|
||||
val values = ContentValues().apply {
|
||||
put(Telephony.Sms.ADDRESS, address)
|
||||
put(Telephony.Sms.BODY, body)
|
||||
put(Telephony.Sms.DATE_SENT, sentTime)
|
||||
put(Telephony.Sms.SUBSCRIPTION_ID, subId)
|
||||
}
|
||||
val uri = context.contentResolver.insert(Telephony.Sms.Inbox.CONTENT_URI, values)
|
||||
|
||||
// Insert the message to Realm
|
||||
return Message().apply {
|
||||
this.address = address
|
||||
this.body = body
|
||||
this.dateSent = sentTime
|
||||
this.date = System.currentTimeMillis()
|
||||
this.subId = subId
|
||||
|
||||
id = messageIds.newId()
|
||||
threadId = conversationRepo.getOrCreateConversation(address)?.id ?: 0L
|
||||
contentId = uri.lastPathSegment.toLong()
|
||||
boxId = Telephony.Sms.MESSAGE_TYPE_INBOX
|
||||
type = "sms"
|
||||
insertOrUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the message as sending, in case we need to retry sending it
|
||||
*/
|
||||
override fun markSending(id: Long) {
|
||||
Realm.getDefaultInstance().use { realm ->
|
||||
realm.refresh()
|
||||
|
||||
val message = realm.where(Message::class.java).equalTo("id", id).findFirst()
|
||||
message?.let {
|
||||
// Update the message in realm
|
||||
realm.executeTransaction {
|
||||
message.boxId = Telephony.Sms.MESSAGE_TYPE_OUTBOX
|
||||
}
|
||||
|
||||
// Update the message in the native ContentProvider
|
||||
val values = ContentValues()
|
||||
values.put(Telephony.Sms.TYPE, Telephony.Sms.MESSAGE_TYPE_OUTBOX)
|
||||
context.contentResolver.update(message.getUri(), values, null, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun markSent(id: Long) {
|
||||
Realm.getDefaultInstance().use { realm ->
|
||||
realm.refresh()
|
||||
|
||||
val message = realm.where(Message::class.java).equalTo("id", id).findFirst()
|
||||
message?.let {
|
||||
// Update the message in realm
|
||||
realm.executeTransaction {
|
||||
message.boxId = Telephony.Sms.MESSAGE_TYPE_SENT
|
||||
}
|
||||
|
||||
// Update the message in the native ContentProvider
|
||||
val values = ContentValues()
|
||||
values.put(Telephony.Sms.TYPE, Telephony.Sms.MESSAGE_TYPE_SENT)
|
||||
context.contentResolver.update(message.getUri(), values, null, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun markFailed(id: Long, resultCode: Int) {
|
||||
Realm.getDefaultInstance().use { realm ->
|
||||
realm.refresh()
|
||||
|
||||
val message = realm.where(Message::class.java).equalTo("id", id).findFirst()
|
||||
message?.let {
|
||||
// Update the message in realm
|
||||
realm.executeTransaction {
|
||||
message.boxId = Telephony.Sms.MESSAGE_TYPE_FAILED
|
||||
message.errorCode = resultCode
|
||||
}
|
||||
|
||||
// Update the message in the native ContentProvider
|
||||
val values = ContentValues()
|
||||
values.put(Telephony.Sms.TYPE, Telephony.Sms.MESSAGE_TYPE_FAILED)
|
||||
values.put(Telephony.Sms.ERROR_CODE, resultCode)
|
||||
context.contentResolver.update(message.getUri(), values, null, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun markDelivered(id: Long) {
|
||||
Realm.getDefaultInstance().use { realm ->
|
||||
realm.refresh()
|
||||
|
||||
val message = realm.where(Message::class.java).equalTo("id", id).findFirst()
|
||||
message?.let {
|
||||
// Update the message in realm
|
||||
realm.executeTransaction {
|
||||
message.deliveryStatus = Telephony.Sms.STATUS_COMPLETE
|
||||
message.dateSent = System.currentTimeMillis()
|
||||
message.read = true
|
||||
}
|
||||
|
||||
// Update the message in the native ContentProvider
|
||||
val values = ContentValues()
|
||||
values.put(Telephony.Sms.STATUS, Telephony.Sms.STATUS_COMPLETE)
|
||||
values.put(Telephony.Sms.DATE_SENT, System.currentTimeMillis())
|
||||
values.put(Telephony.Sms.READ, true)
|
||||
context.contentResolver.update(message.getUri(), values, null, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun markDeliveryFailed(id: Long, resultCode: Int) {
|
||||
Realm.getDefaultInstance().use { realm ->
|
||||
realm.refresh()
|
||||
|
||||
val message = realm.where(Message::class.java).equalTo("id", id).findFirst()
|
||||
message?.let {
|
||||
// Update the message in realm
|
||||
realm.executeTransaction {
|
||||
message.deliveryStatus = Telephony.Sms.STATUS_FAILED
|
||||
message.dateSent = System.currentTimeMillis()
|
||||
message.read = true
|
||||
message.errorCode = resultCode
|
||||
}
|
||||
|
||||
// Update the message in the native ContentProvider
|
||||
val values = ContentValues()
|
||||
values.put(Telephony.Sms.STATUS, Telephony.Sms.STATUS_FAILED)
|
||||
values.put(Telephony.Sms.DATE_SENT, System.currentTimeMillis())
|
||||
values.put(Telephony.Sms.READ, true)
|
||||
values.put(Telephony.Sms.ERROR_CODE, resultCode)
|
||||
context.contentResolver.update(message.getUri(), values, null, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun deleteMessages(vararg messageIds: Long) {
|
||||
Realm.getDefaultInstance().use { realm ->
|
||||
realm.refresh()
|
||||
|
||||
val messages = realm.where(Message::class.java)
|
||||
.anyOf("id", messageIds)
|
||||
.findAll()
|
||||
|
||||
val uris = messages.map { it.getUri() }
|
||||
|
||||
realm.executeTransaction { messages.deleteAllFromRealm() }
|
||||
|
||||
uris.forEach { uri -> context.contentResolver.delete(uri, null, null) }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.moez.QKSMS.repository
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.ContentUris
|
||||
import android.net.Uri
|
||||
import android.provider.Telephony
|
||||
import android.telephony.PhoneNumberUtils
|
||||
import com.f2prateek.rx.preferences2.RxSharedPreferences
|
||||
import com.moez.QKSMS.extensions.insertOrUpdate
|
||||
import com.moez.QKSMS.extensions.map
|
||||
import com.moez.QKSMS.manager.KeyManager
|
||||
import com.moez.QKSMS.mapper.CursorToContact
|
||||
import com.moez.QKSMS.mapper.CursorToConversation
|
||||
import com.moez.QKSMS.mapper.CursorToMessage
|
||||
import com.moez.QKSMS.mapper.CursorToRecipient
|
||||
import com.moez.QKSMS.model.Contact
|
||||
import com.moez.QKSMS.model.Conversation
|
||||
import com.moez.QKSMS.model.Message
|
||||
import com.moez.QKSMS.model.MmsPart
|
||||
import com.moez.QKSMS.model.Recipient
|
||||
import com.moez.QKSMS.model.SyncLog
|
||||
import com.moez.QKSMS.util.tryOrNull
|
||||
import io.reactivex.subjects.BehaviorSubject
|
||||
import io.reactivex.subjects.Subject
|
||||
import io.realm.Realm
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class SyncRepositoryImpl @Inject constructor(
|
||||
private val contentResolver: ContentResolver,
|
||||
private val conversationRepo: ConversationRepository,
|
||||
private val messageRepo: MessageRepository,
|
||||
private val cursorToConversation: CursorToConversation,
|
||||
private val cursorToMessage: CursorToMessage,
|
||||
private val cursorToRecipient: CursorToRecipient,
|
||||
private val cursorToContact: CursorToContact,
|
||||
private val keys: KeyManager,
|
||||
private val rxPrefs: RxSharedPreferences
|
||||
) : SyncRepository {
|
||||
|
||||
/**
|
||||
* Holds data that should be persisted across full syncs
|
||||
*/
|
||||
private data class PersistedData(
|
||||
val id: Long,
|
||||
val archived: Boolean,
|
||||
val blocked: Boolean,
|
||||
val name: String)
|
||||
|
||||
override val syncProgress: Subject<SyncRepository.SyncProgress> = BehaviorSubject.createDefault(SyncRepository.SyncProgress.Idle())
|
||||
|
||||
override fun syncMessages() {
|
||||
|
||||
// If the sync is already running, don't try to do another one
|
||||
if (syncProgress.blockingFirst() is SyncRepository.SyncProgress.Running) return
|
||||
syncProgress.onNext(SyncRepository.SyncProgress.Running(0f))
|
||||
|
||||
val realm = Realm.getDefaultInstance()
|
||||
realm.beginTransaction()
|
||||
|
||||
var persistedData = realm.where(Conversation::class.java)
|
||||
.beginGroup()
|
||||
.equalTo("archived", true)
|
||||
.or()
|
||||
.equalTo("blocked", true)
|
||||
.or()
|
||||
.isNotEmpty("name")
|
||||
.endGroup()
|
||||
.findAll()
|
||||
.map { PersistedData(it.id, it.archived, it.blocked, it.name) }
|
||||
|
||||
realm.delete(Contact::class.java)
|
||||
realm.delete(Conversation::class.java)
|
||||
realm.delete(Message::class.java)
|
||||
realm.delete(MmsPart::class.java)
|
||||
realm.delete(Recipient::class.java)
|
||||
|
||||
keys.reset()
|
||||
|
||||
|
||||
// Sync messages
|
||||
cursorToMessage.getMessagesCursor()?.use { messageCursor ->
|
||||
val messageColumns = CursorToMessage.MessageColumns(messageCursor)
|
||||
val messages = messageCursor.map { cursor -> cursorToMessage.map(Pair(cursor, messageColumns)) }
|
||||
realm.insertOrUpdate(messages)
|
||||
}
|
||||
|
||||
// Migrate blocked conversations from 2.7.3
|
||||
val oldBlockedSenders = rxPrefs.getStringSet("pref_key_blocked_senders")
|
||||
persistedData += oldBlockedSenders.get()
|
||||
.map { threadIdString -> threadIdString.toLong() }
|
||||
.filter { threadId -> persistedData.none { it.id == threadId } }
|
||||
.map { threadId -> PersistedData(threadId, false, true, "") }
|
||||
|
||||
// Sync conversations
|
||||
cursorToConversation.getConversationsCursor()?.use { conversationCursor ->
|
||||
val conversations = conversationCursor
|
||||
.map { cursor -> cursorToConversation.map(cursor) }
|
||||
|
||||
persistedData.forEach { data ->
|
||||
val conversation = conversations.firstOrNull { conversation -> conversation.id == data.id }
|
||||
conversation?.archived = data.archived
|
||||
conversation?.blocked = data.blocked
|
||||
conversation?.name = data.name
|
||||
}
|
||||
|
||||
realm.insertOrUpdate(conversations)
|
||||
}
|
||||
|
||||
|
||||
// Sync recipients
|
||||
cursorToRecipient.getRecipientCursor()?.use { recipientCursor ->
|
||||
val contacts = realm.copyToRealm(getContacts())
|
||||
val recipients = recipientCursor
|
||||
.map { cursor -> cursorToRecipient.map(cursor) }
|
||||
.map { recipient ->
|
||||
recipient.apply {
|
||||
contact = contacts.firstOrNull { contact ->
|
||||
contact.numbers.any { PhoneNumberUtils.compare(recipient.address, it.address) }
|
||||
}
|
||||
}
|
||||
}
|
||||
realm.insertOrUpdate(recipients)
|
||||
}
|
||||
|
||||
|
||||
realm.insert(SyncLog())
|
||||
realm.commitTransaction()
|
||||
realm.close()
|
||||
|
||||
// Only delete this after the sync has successfully completed
|
||||
oldBlockedSenders.delete()
|
||||
|
||||
syncProgress.onNext(SyncRepository.SyncProgress.Idle())
|
||||
}
|
||||
|
||||
override fun syncMessage(uri: Uri): Message? {
|
||||
|
||||
// If we don't have a valid type, return null
|
||||
val type = when {
|
||||
uri.toString().contains("mms") -> "mms"
|
||||
uri.toString().contains("sms") -> "sms"
|
||||
else -> return null
|
||||
}
|
||||
|
||||
// If we don't have a valid id, return null
|
||||
val id = tryOrNull { ContentUris.parseId(uri) } ?: return null
|
||||
|
||||
// Check if the message already exists, so we can reuse the id
|
||||
val existingId = Realm.getDefaultInstance().use { realm ->
|
||||
realm.refresh()
|
||||
realm.where(Message::class.java)
|
||||
.equalTo("type", type)
|
||||
.equalTo("contentId", id)
|
||||
.findFirst()
|
||||
?.id
|
||||
}
|
||||
|
||||
// The uri might be something like content://mms/inbox/id
|
||||
// The box might change though, so we should just use the mms/id uri
|
||||
val stableUri = when (type) {
|
||||
"mms" -> ContentUris.withAppendedId(Telephony.Mms.CONTENT_URI, id)
|
||||
else -> ContentUris.withAppendedId(Telephony.Sms.CONTENT_URI, id)
|
||||
}
|
||||
|
||||
return contentResolver.query(stableUri, null, null, null, null)?.use { cursor ->
|
||||
|
||||
// If there are no rows, return null. Otherwise, we've moved to the first row
|
||||
if (!cursor.moveToFirst()) return null
|
||||
|
||||
val columnsMap = CursorToMessage.MessageColumns(cursor)
|
||||
cursorToMessage.map(Pair(cursor, columnsMap)).apply {
|
||||
existingId?.let { this.id = it }
|
||||
|
||||
conversationRepo.getOrCreateConversation(threadId)
|
||||
insertOrUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun syncContacts() {
|
||||
// Load all the contacts
|
||||
var contacts = getContacts()
|
||||
|
||||
Realm.getDefaultInstance()?.use { realm ->
|
||||
val recipients = realm.where(Recipient::class.java).findAll()
|
||||
|
||||
realm.executeTransaction {
|
||||
realm.delete(Contact::class.java)
|
||||
|
||||
contacts = realm.copyToRealm(contacts)
|
||||
|
||||
// Update all the recipients with the new contacts
|
||||
val updatedRecipients = recipients.map { recipient ->
|
||||
recipient.apply {
|
||||
contact = contacts.firstOrNull {
|
||||
it.numbers.any { PhoneNumberUtils.compare(recipient.address, it.address) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
realm.insertOrUpdate(updatedRecipients)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private fun getContacts(): List<Contact> {
|
||||
return cursorToContact.getContactsCursor()
|
||||
?.map { cursor -> cursorToContact.map(cursor) }
|
||||
?.groupBy { contact -> contact.lookupKey }
|
||||
?.map { contacts ->
|
||||
val allNumbers = contacts.value.map { it.numbers }.flatten()
|
||||
contacts.value.first().apply {
|
||||
numbers.clear()
|
||||
numbers.addAll(allNumbers)
|
||||
}
|
||||
} ?: listOf()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.moez.QKSMS.service
|
||||
|
||||
import android.app.IntentService
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.telephony.TelephonyManager
|
||||
import com.moez.QKSMS.interactor.SendMessage
|
||||
import com.moez.QKSMS.repository.ConversationRepository
|
||||
import dagger.android.AndroidInjection
|
||||
import javax.inject.Inject
|
||||
|
||||
class HeadlessSmsSendService : IntentService("HeadlessSmsSendService") {
|
||||
|
||||
@Inject lateinit var conversationRepo: ConversationRepository
|
||||
@Inject lateinit var sendMessage: SendMessage
|
||||
|
||||
override fun onHandleIntent(intent: Intent?) {
|
||||
if (intent?.action != TelephonyManager.ACTION_RESPOND_VIA_MESSAGE) return
|
||||
|
||||
AndroidInjection.inject(this)
|
||||
intent.extras?.getString(Intent.EXTRA_TEXT)?.takeIf { it.isNotBlank() }?.let { body ->
|
||||
val intentUri = intent.data
|
||||
val recipients = getRecipients(intentUri).split(";")
|
||||
val threadId = conversationRepo.getOrCreateConversation(recipients)?.id ?: 0L
|
||||
sendMessage.execute(SendMessage.Params(-1, threadId, recipients, body))
|
||||
}
|
||||
}
|
||||
|
||||
private fun getRecipients(uri: Uri): String {
|
||||
val base = uri.schemeSpecificPart
|
||||
val position = base.indexOf('?')
|
||||
return if (position == -1) base else base.substring(0, position)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.moez.QKSMS.util
|
||||
|
||||
import com.moez.QKSMS.compat.SubscriptionInfoCompat
|
||||
import com.moez.QKSMS.compat.SubscriptionManagerCompat
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Observer
|
||||
import io.reactivex.disposables.Disposable
|
||||
|
||||
class ActiveSubscriptionObservable(
|
||||
private val subscriptionManager: SubscriptionManagerCompat
|
||||
) : Observable<List<SubscriptionInfoCompat>>() {
|
||||
|
||||
override fun subscribeActual(observer: Observer<in List<SubscriptionInfoCompat>>) {
|
||||
observer.onNext(subscriptionManager.activeSubscriptionInfoList)
|
||||
|
||||
val listener = Listener(subscriptionManager, observer)
|
||||
observer.onSubscribe(listener)
|
||||
subscriptionManager.addOnSubscriptionsChangedListener(listener)
|
||||
}
|
||||
|
||||
internal class Listener(
|
||||
private val subscriptionManager: SubscriptionManagerCompat,
|
||||
private val observer: Observer<in List<SubscriptionInfoCompat>>
|
||||
) : Disposable, SubscriptionManagerCompat.OnSubscriptionsChangedListener() {
|
||||
|
||||
private var disposed: Boolean = false
|
||||
|
||||
override fun onSubscriptionsChanged() {
|
||||
if (!isDisposed) {
|
||||
observer.onNext(subscriptionManager.activeSubscriptionInfoList)
|
||||
}
|
||||
}
|
||||
|
||||
override fun isDisposed(): Boolean = disposed
|
||||
|
||||
override fun dispose() {
|
||||
disposed = true
|
||||
subscriptionManager.removeOnSubscriptionsChangedListener(this)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.moez.QKSMS.util
|
||||
|
||||
import android.app.AlarmManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.moez.QKSMS.receiver.NightModeReceiver
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class NightModeManager @Inject constructor(
|
||||
private val context: Context,
|
||||
private val prefs: Preferences) {
|
||||
|
||||
fun updateCurrentTheme() {
|
||||
// If night mode is not on auto, then there's nothing to do here
|
||||
if (prefs.nightMode.get() != Preferences.NIGHT_MODE_AUTO) {
|
||||
return
|
||||
}
|
||||
|
||||
val nightStartTime = getPreviousInstanceOfTime(prefs.nightStart.get())
|
||||
val nightEndTime = getPreviousInstanceOfTime(prefs.nightEnd.get())
|
||||
|
||||
// If the last nightStart was more recent than the last nightEnd, then it's night time
|
||||
prefs.night.set(nightStartTime > nightEndTime)
|
||||
}
|
||||
|
||||
fun updateNightMode(mode: Int) {
|
||||
prefs.nightMode.set(mode)
|
||||
|
||||
// If it's not on auto mode, set the appropriate night mode
|
||||
if (mode != Preferences.NIGHT_MODE_AUTO) {
|
||||
prefs.night.set(mode == Preferences.NIGHT_MODE_ON)
|
||||
}
|
||||
|
||||
updateAlarms()
|
||||
}
|
||||
|
||||
fun setNightStart(hour: Int, minute: Int) {
|
||||
prefs.nightStart.set("$hour:$minute")
|
||||
updateAlarms()
|
||||
}
|
||||
|
||||
fun setNightEnd(hour: Int, minute: Int) {
|
||||
prefs.nightEnd.set("$hour:$minute")
|
||||
updateAlarms()
|
||||
}
|
||||
|
||||
private fun updateAlarms() {
|
||||
val dayCalendar = createCalendar(prefs.nightEnd.get())
|
||||
val day = Intent(context, NightModeReceiver::class.java)
|
||||
val dayIntent = PendingIntent.getBroadcast(context, 0, day, 0)
|
||||
|
||||
val nightCalendar = createCalendar(prefs.nightStart.get())
|
||||
val night = Intent(context, NightModeReceiver::class.java)
|
||||
val nightIntent = PendingIntent.getBroadcast(context, 1, night, 0)
|
||||
|
||||
context.sendBroadcast(day)
|
||||
context.sendBroadcast(night)
|
||||
|
||||
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
if (prefs.nightMode.get() == Preferences.NIGHT_MODE_AUTO) {
|
||||
alarmManager.setInexactRepeating(AlarmManager.RTC_WAKEUP, dayCalendar.timeInMillis, AlarmManager.INTERVAL_DAY, dayIntent)
|
||||
alarmManager.setInexactRepeating(AlarmManager.RTC_WAKEUP, nightCalendar.timeInMillis, AlarmManager.INTERVAL_DAY, nightIntent)
|
||||
} else {
|
||||
alarmManager.cancel(dayIntent)
|
||||
alarmManager.cancel(nightIntent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createCalendar(time: String): Calendar {
|
||||
val calendar = parseTime(time)
|
||||
|
||||
return Calendar.getInstance().apply {
|
||||
set(Calendar.HOUR_OF_DAY, calendar.get(Calendar.HOUR_OF_DAY))
|
||||
set(Calendar.MINUTE, calendar.get(Calendar.MINUTE))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the hour and minute out of the [time], which should be formatted h:mm
|
||||
*/
|
||||
fun parseTime(time: String): Calendar {
|
||||
return tryOrNull {
|
||||
val parsedTime = SimpleDateFormat("H:mm", Locale.US).parse(time)
|
||||
Calendar.getInstance().apply { this.time = parsedTime }
|
||||
} ?: tryOrNull {
|
||||
// Parse the legacy timestamp format (<=3.1.3)
|
||||
val parsedTime = SimpleDateFormat("h:mm a", Locale.US).parse(time)
|
||||
Calendar.getInstance().apply { this.time = parsedTime }
|
||||
} ?: Calendar.getInstance().apply { set(Calendar.HOUR_OF_DAY, 18) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Calendar set to the most recent occurrence of this time
|
||||
*/
|
||||
private fun getPreviousInstanceOfTime(time: String): Calendar {
|
||||
val currentTime = System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(10)
|
||||
val calendar = createCalendar(time)
|
||||
|
||||
while (calendar.timeInMillis > currentTime) {
|
||||
calendar.add(Calendar.DAY_OF_YEAR, -1)
|
||||
}
|
||||
|
||||
return calendar
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.moez.QKSMS.util
|
||||
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import com.f2prateek.rx.preferences2.Preference
|
||||
import com.f2prateek.rx.preferences2.RxSharedPreferences
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class Preferences @Inject constructor(private val rxPrefs: RxSharedPreferences) {
|
||||
|
||||
companion object {
|
||||
const val NIGHT_MODE_OFF = 0
|
||||
const val NIGHT_MODE_ON = 1
|
||||
const val NIGHT_MODE_AUTO = 2
|
||||
|
||||
const val TEXT_SIZE_SMALL = 0
|
||||
const val TEXT_SIZE_NORMAL = 1
|
||||
const val TEXT_SIZE_LARGE = 2
|
||||
const val TEXT_SIZE_LARGER = 3
|
||||
|
||||
const val NOTIFICATION_PREVIEWS_ALL = 0
|
||||
const val NOTIFICATION_PREVIEWS_NAME = 1
|
||||
const val NOTIFICATION_PREVIEWS_NONE = 2
|
||||
|
||||
const val SEND_DELAY_NONE = 0
|
||||
const val SEND_DELAY_SHORT = 1
|
||||
const val SEND_DELAY_MEDIUM = 2
|
||||
const val SEND_DELAY_LONG = 3
|
||||
}
|
||||
|
||||
val night = rxPrefs.getBoolean("night", false)
|
||||
val nightMode = rxPrefs.getInteger("nightModeSummary", NIGHT_MODE_OFF)
|
||||
val nightStart = rxPrefs.getString("nightStart", "18:00")
|
||||
val nightEnd = rxPrefs.getString("nightEnd", "6:00")
|
||||
val black = rxPrefs.getBoolean("black", false)
|
||||
val sia = rxPrefs.getBoolean("sia", false)
|
||||
val sendDelay = rxPrefs.getInteger("sendDelay", SEND_DELAY_NONE)
|
||||
val autoEmoji = rxPrefs.getBoolean("autoEmoji", true)
|
||||
val delivery = rxPrefs.getBoolean("delivery", false)
|
||||
val systemFont = rxPrefs.getBoolean("systemFont", false)
|
||||
val textSize = rxPrefs.getInteger("textSize", TEXT_SIZE_NORMAL)
|
||||
val qkreply = rxPrefs.getBoolean("qkreply", Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
|
||||
val qkreplyTapDismiss = rxPrefs.getBoolean("qkreplyTapDismiss", true)
|
||||
val unicode = rxPrefs.getBoolean("unicode", false)
|
||||
val mmsSize = rxPrefs.getInteger("mmsSize", 300)
|
||||
|
||||
fun theme(threadId: Long = 0): Preference<Int> {
|
||||
val default = rxPrefs.getInteger("theme", 0xFF0097A7.toInt())
|
||||
|
||||
return when (threadId) {
|
||||
0L -> default
|
||||
else -> rxPrefs.getInteger("theme_$threadId", default.get())
|
||||
}
|
||||
}
|
||||
|
||||
fun notifications(threadId: Long = 0): Preference<Boolean> {
|
||||
val default = rxPrefs.getBoolean("notifications", true)
|
||||
|
||||
return when (threadId) {
|
||||
0L -> default
|
||||
else -> rxPrefs.getBoolean("notifications_$threadId", default.get())
|
||||
}
|
||||
}
|
||||
|
||||
fun notificationPreviews(threadId: Long = 0): Preference<Int> {
|
||||
val default = rxPrefs.getInteger("notification_previews", 0)
|
||||
|
||||
return when (threadId) {
|
||||
0L -> default
|
||||
else -> rxPrefs.getInteger("notification_previews_$threadId", default.get())
|
||||
}
|
||||
}
|
||||
|
||||
fun vibration(threadId: Long = 0): Preference<Boolean> {
|
||||
val default = rxPrefs.getBoolean("vibration", true)
|
||||
|
||||
return when (threadId) {
|
||||
0L -> default
|
||||
else -> rxPrefs.getBoolean("vibration$threadId", default.get())
|
||||
}
|
||||
}
|
||||
|
||||
fun ringtone(threadId: Long = 0): Preference<String> {
|
||||
val default = rxPrefs.getString("ringtone", Settings.System.DEFAULT_NOTIFICATION_URI.toString())
|
||||
|
||||
return when (threadId) {
|
||||
0L -> default
|
||||
else -> rxPrefs.getString("ringtone_$threadId", default.get())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.moez.QKSMS.util
|
||||
|
||||
fun <T> tryOrNull(body: () -> T?): T? {
|
||||
return try {
|
||||
body()
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user