From bb816209ba00be5e4cd6dabca700e9a5a29934aa Mon Sep 17 00:00:00 2001 From: moezbhatti Date: Thu, 7 Nov 2019 02:44:19 -0500 Subject: [PATCH] Build phone number picker --- data/build.gradle | 1 - .../moez/QKSMS/mapper/CursorToContactImpl.kt | 19 ++-- .../moez/QKSMS/migration/QkRealmMigration.kt | 2 + .../QKSMS/repository/ContactRepositoryImpl.kt | 16 +++ .../QKSMS/repository/SyncRepositoryImpl.kt | 11 +- domain/build.gradle | 1 - .../QKSMS/interactor/SetDefaultPhoneNumber.kt | 38 +++++++ .../main/java/com/moez/QKSMS/model/Contact.kt | 6 +- .../java/com/moez/QKSMS/model/PhoneNumber.kt | 7 +- .../QKSMS/repository/ContactRepository.kt | 4 +- .../com/moez/QKSMS/common/widget/QkDialog.kt | 105 ++++++++++++++++++ .../QKSMS/feature/compose/ComposeActivity.kt | 39 ++++++- .../QKSMS/feature/compose/ComposeState.kt | 2 + .../moez/QKSMS/feature/compose/ComposeView.kt | 7 +- .../QKSMS/feature/compose/ComposeViewModel.kt | 48 +++++++- .../compose/editing/ComposeItemAdapter.kt | 10 +- .../compose/editing/PhoneNumberAction.kt | 25 +++++ .../editing/PhoneNumberPickerAdapter.kt | 77 +++++++++++++ .../res/layout/phone_number_list_item.xml | 33 ++++++ .../src/main/res/layout/qk_dialog.xml | 104 +++++++++++++++++ presentation/src/main/res/values/strings.xml | 4 + 21 files changed, 531 insertions(+), 28 deletions(-) create mode 100644 domain/src/main/java/com/moez/QKSMS/interactor/SetDefaultPhoneNumber.kt create mode 100644 presentation/src/main/java/com/moez/QKSMS/common/widget/QkDialog.kt create mode 100644 presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/PhoneNumberAction.kt create mode 100644 presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/PhoneNumberPickerAdapter.kt create mode 100644 presentation/src/main/res/layout/phone_number_list_item.xml create mode 100644 presentation/src/main/res/layout/qk_dialog.xml diff --git a/data/build.gradle b/data/build.gradle index ee2b5fe7..e023ae7a 100644 --- a/data/build.gradle +++ b/data/build.gradle @@ -79,7 +79,6 @@ dependencies { // coroutines implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-reactive:$coroutines_version" diff --git a/data/src/main/java/com/moez/QKSMS/mapper/CursorToContactImpl.kt b/data/src/main/java/com/moez/QKSMS/mapper/CursorToContactImpl.kt index 62f1602d..23ba4e5f 100644 --- a/data/src/main/java/com/moez/QKSMS/mapper/CursorToContactImpl.kt +++ b/data/src/main/java/com/moez/QKSMS/mapper/CursorToContactImpl.kt @@ -34,6 +34,7 @@ class CursorToContactImpl @Inject constructor( companion object { val URI = Phone.CONTENT_URI val PROJECTION = arrayOf( + Phone._ID, Phone.LOOKUP_KEY, Phone.ACCOUNT_TYPE_AND_DATA_SET, Phone.NUMBER, @@ -44,20 +45,22 @@ class CursorToContactImpl @Inject constructor( Phone.CONTACT_LAST_UPDATED_TIMESTAMP ) - const val COLUMN_LOOKUP_KEY = 0 - const val COLUMN_ACCOUNT_TYPE = 1 - const val COLUMN_NUMBER = 2 - const val COLUMN_TYPE = 3 - const val COLUMN_LABEL = 4 - const val COLUMN_DISPLAY_NAME = 5 - const val COLUMN_STARRED = 6 - const val CONTACT_LAST_UPDATED = 7 + const val COLUMN_ID = 0 + const val COLUMN_LOOKUP_KEY = 1 + const val COLUMN_ACCOUNT_TYPE = 2 + const val COLUMN_NUMBER = 3 + const val COLUMN_TYPE = 4 + const val COLUMN_LABEL = 5 + const val COLUMN_DISPLAY_NAME = 6 + const val COLUMN_STARRED = 7 + const val CONTACT_LAST_UPDATED = 8 } override fun map(from: Cursor) = Contact().apply { lookupKey = from.getString(COLUMN_LOOKUP_KEY) name = from.getString(COLUMN_DISPLAY_NAME) ?: "" numbers.add(PhoneNumber( + id = from.getLong(COLUMN_ID), accountType = from.getString(COLUMN_ACCOUNT_TYPE), address = from.getString(COLUMN_NUMBER) ?: "", type = Phone.getTypeLabel(context.resources, from.getInt(COLUMN_TYPE), diff --git a/data/src/main/java/com/moez/QKSMS/migration/QkRealmMigration.kt b/data/src/main/java/com/moez/QKSMS/migration/QkRealmMigration.kt index 2cf98f1f..06a10c53 100644 --- a/data/src/main/java/com/moez/QKSMS/migration/QkRealmMigration.kt +++ b/data/src/main/java/com/moez/QKSMS/migration/QkRealmMigration.kt @@ -128,7 +128,9 @@ class QkRealmMigration : RealmMigration { ?.addField("starred", Boolean::class.java, FieldAttribute.REQUIRED) realm.schema.get("PhoneNumber") + ?.addField("id", Long::class.java, FieldAttribute.PRIMARY_KEY, FieldAttribute.REQUIRED) ?.addField("accountType", String::class.java, FieldAttribute.REQUIRED) + ?.addField("isDefault", Boolean::class.java, FieldAttribute.REQUIRED) version++ } diff --git a/data/src/main/java/com/moez/QKSMS/repository/ContactRepositoryImpl.kt b/data/src/main/java/com/moez/QKSMS/repository/ContactRepositoryImpl.kt index 842a423c..268913a6 100644 --- a/data/src/main/java/com/moez/QKSMS/repository/ContactRepositoryImpl.kt +++ b/data/src/main/java/com/moez/QKSMS/repository/ContactRepositoryImpl.kt @@ -137,4 +137,20 @@ class ContactRepositoryImpl @Inject constructor( .observeOn(Schedulers.io()) } + override fun setDefaultPhoneNumber(lookupKey: String, phoneNumberId: Long) { + Realm.getDefaultInstance().use { realm -> + realm.refresh() + val contact = realm.where(Contact::class.java) + .equalTo("lookupKey", lookupKey) + .findFirst() + ?: return + + realm.executeTransaction { + contact.numbers.forEach { number -> + number.isDefault = number.id == phoneNumberId + } + } + } + } + } diff --git a/data/src/main/java/com/moez/QKSMS/repository/SyncRepositoryImpl.kt b/data/src/main/java/com/moez/QKSMS/repository/SyncRepositoryImpl.kt index 77bd2f9e..3748bdcb 100644 --- a/data/src/main/java/com/moez/QKSMS/repository/SyncRepositoryImpl.kt +++ b/data/src/main/java/com/moez/QKSMS/repository/SyncRepositoryImpl.kt @@ -243,7 +243,7 @@ class SyncRepositoryImpl @Inject constructor( realm.delete(Contact::class.java) realm.delete(ContactGroup::class.java) - contacts = realm.copyToRealm(contacts) + contacts = realm.copyToRealmOrUpdate(contacts) realm.insertOrUpdate(getContactGroups(contacts)) // Update all the recipients with the new contacts @@ -286,6 +286,13 @@ class SyncRepositoryImpl @Inject constructor( } private fun getContacts(): List { + val defaultNumberIds = Realm.getDefaultInstance().use { realm -> + realm.where(PhoneNumber::class.java) + .equalTo("isDefault", true) + .findAll() + .map { number -> number.id } + } + return cursorToContact.getContactsCursor() ?.map { cursor -> cursorToContact.map(cursor) } ?.groupBy { contact -> contact.lookupKey } @@ -297,10 +304,12 @@ class SyncRepositoryImpl @Inject constructor( .flatMap { it.numbers } .sortedBy { it.accountType } .forEach { number -> + number.isDefault = defaultNumberIds.any { id -> id == number.id } val duplicate = uniqueNumbers.any { other -> number.accountType != other.accountType && phoneNumberUtils.compare(number.address, other.address) } + if (!duplicate) { uniqueNumbers += number } diff --git a/domain/build.gradle b/domain/build.gradle index f1098d77..97817fa4 100644 --- a/domain/build.gradle +++ b/domain/build.gradle @@ -53,7 +53,6 @@ dependencies { // coroutines implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-reactive:$coroutines_version" diff --git a/domain/src/main/java/com/moez/QKSMS/interactor/SetDefaultPhoneNumber.kt b/domain/src/main/java/com/moez/QKSMS/interactor/SetDefaultPhoneNumber.kt new file mode 100644 index 00000000..639c1542 --- /dev/null +++ b/domain/src/main/java/com/moez/QKSMS/interactor/SetDefaultPhoneNumber.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2019 Moez Bhatti + * + * This file is part of QKSMS. + * + * QKSMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QKSMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with QKSMS. If not, see . + */ +package com.moez.QKSMS.interactor + +import com.moez.QKSMS.repository.ContactRepository +import io.reactivex.Flowable +import javax.inject.Inject + +class SetDefaultPhoneNumber @Inject constructor( + private val contactRepo: ContactRepository +) : Interactor() { + + data class Params(val lookupKey: String, val phoneNumberId: Long) + + override fun buildObservable(params: Params): Flowable<*> { + return Flowable.just(params) + .doOnNext { (lookupKey, phoneNumberId) -> + contactRepo.setDefaultPhoneNumber(lookupKey, phoneNumberId) + } + } + +} diff --git a/domain/src/main/java/com/moez/QKSMS/model/Contact.kt b/domain/src/main/java/com/moez/QKSMS/model/Contact.kt index dd325dac..7e8cd27c 100644 --- a/domain/src/main/java/com/moez/QKSMS/model/Contact.kt +++ b/domain/src/main/java/com/moez/QKSMS/model/Contact.kt @@ -28,4 +28,8 @@ open class Contact( var name: String = "", var starred: Boolean = false, var lastUpdate: Long = 0 -) : RealmObject() \ No newline at end of file +) : RealmObject() { + + fun getDefaultNumber(): PhoneNumber? = numbers.find { number -> number.isDefault } + +} diff --git a/domain/src/main/java/com/moez/QKSMS/model/PhoneNumber.kt b/domain/src/main/java/com/moez/QKSMS/model/PhoneNumber.kt index db02b9ef..2669049d 100644 --- a/domain/src/main/java/com/moez/QKSMS/model/PhoneNumber.kt +++ b/domain/src/main/java/com/moez/QKSMS/model/PhoneNumber.kt @@ -19,9 +19,12 @@ package com.moez.QKSMS.model import io.realm.RealmObject +import io.realm.annotations.PrimaryKey open class PhoneNumber( + @PrimaryKey var id: Long = 0, var accountType: String = "", var address: String = "", - var type: String = "" -) : RealmObject() \ No newline at end of file + var type: String = "", + var isDefault: Boolean = false +) : RealmObject() diff --git a/domain/src/main/java/com/moez/QKSMS/repository/ContactRepository.kt b/domain/src/main/java/com/moez/QKSMS/repository/ContactRepository.kt index 0bc8471b..4242afb5 100644 --- a/domain/src/main/java/com/moez/QKSMS/repository/ContactRepository.kt +++ b/domain/src/main/java/com/moez/QKSMS/repository/ContactRepository.kt @@ -35,4 +35,6 @@ interface ContactRepository { fun getUnmanagedContactGroups(): Observable> -} \ No newline at end of file + fun setDefaultPhoneNumber(lookupKey: String, phoneNumberId: Long) + +} diff --git a/presentation/src/main/java/com/moez/QKSMS/common/widget/QkDialog.kt b/presentation/src/main/java/com/moez/QKSMS/common/widget/QkDialog.kt new file mode 100644 index 00000000..f275c8a0 --- /dev/null +++ b/presentation/src/main/java/com/moez/QKSMS/common/widget/QkDialog.kt @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2019 Moez Bhatti + * + * This file is part of QKSMS. + * + * QKSMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QKSMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with QKSMS. If not, see . + */ +package com.moez.QKSMS.common.widget + +import android.app.Activity +import android.view.LayoutInflater +import androidx.annotation.StringRes +import androidx.appcompat.app.AlertDialog +import androidx.core.view.isVisible +import com.moez.QKSMS.R +import com.moez.QKSMS.common.base.QkAdapter +import kotlinx.android.synthetic.main.qk_dialog.view.* + +class QkDialog(private val context: Activity) : AlertDialog(context) { + + private val view = LayoutInflater.from(context).inflate(R.layout.qk_dialog, null) + + @StringRes + var titleRes: Int? = null + set(value) { + field = value + title = value?.let(context::getString) + } + + var title: String? = null + set(value) { + field = value + view.title.text = value + view.title.isVisible = !value.isNullOrBlank() + } + + @StringRes + var subtitleRes: Int? = null + set(value) { + field = value + subtitle = value?.let(context::getString) + } + + var subtitle: String? = null + set(value) { + field = value + view.subtitle.text = value + view.subtitle.isVisible = !value.isNullOrBlank() + } + + var adapter: QkAdapter<*>? = null + set(value) { + field = value + view.list.isVisible = value != null + view.list.adapter = value + } + + var positiveButtonListener: (() -> Unit)? = null + + @StringRes + var positiveButton: Int? = null + set(value) { + field = value + value?.run(view.positiveButton::setText) + view.positiveButton.isVisible = value != null + view.positiveButton.setOnClickListener { + positiveButtonListener?.invoke() ?: dismiss() + } + } + + var negativeButtonListener: (() -> Unit)? = null + + @StringRes + var negativeButton: Int? = null + set(value) { + field = value + value?.run(view.negativeButton::setText) + view.negativeButton.isVisible = value != null + view.negativeButton.setOnClickListener { + negativeButtonListener?.invoke() ?: dismiss() + } + } + + var cancelListener: (() -> Unit)? = null + set(value) { + field = value + setOnCancelListener { value?.invoke() } + } + + init { + setView(view) + } + +} diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeActivity.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeActivity.kt index d8aa4bcd..02d39543 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeActivity.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeActivity.kt @@ -54,10 +54,14 @@ import com.moez.QKSMS.common.util.extensions.setBackgroundTint import com.moez.QKSMS.common.util.extensions.setTint import com.moez.QKSMS.common.util.extensions.setVisible import com.moez.QKSMS.common.util.extensions.showKeyboard +import com.moez.QKSMS.common.widget.QkDialog +import com.moez.QKSMS.extensions.Optional import com.moez.QKSMS.feature.compose.editing.Chip import com.moez.QKSMS.feature.compose.editing.ChipsAdapter import com.moez.QKSMS.feature.compose.editing.ComposeItem import com.moez.QKSMS.feature.compose.editing.ComposeItemAdapter +import com.moez.QKSMS.feature.compose.editing.PhoneNumberAction +import com.moez.QKSMS.feature.compose.editing.PhoneNumberPickerAdapter import com.moez.QKSMS.model.Attachment import com.uber.autodispose.android.lifecycle.scope import com.uber.autodispose.autoDisposable @@ -84,13 +88,17 @@ class ComposeActivity : QkThemedActivity(), ComposeView { @Inject lateinit var dateFormatter: DateFormatter @Inject lateinit var messageAdapter: MessagesAdapter @Inject lateinit var navigator: Navigator + @Inject lateinit var phoneNumberAdapter: PhoneNumberPickerAdapter @Inject lateinit var viewModelFactory: ViewModelProvider.Factory override val activityVisibleIntent: Subject = PublishSubject.create() override val queryChangedIntent: Observable by lazy { search.textChanges() } override val queryBackspaceIntent: Observable<*> by lazy { search.backspaces } override val queryEditorActionIntent: Observable by lazy { search.editorActions() } - override val chipSelectedIntent: Subject by lazy { contactsAdapter.itemSelected } + override val composeItemPressedIntent: Subject by lazy { contactsAdapter.clicks } + override val composeItemLongPressedIntent: Subject by lazy { contactsAdapter.longClicks } + override val phoneNumberSelectedIntent: Subject> by lazy { phoneNumberAdapter.selectedItemChanges } + override val phoneNumberActionIntent: Subject = PublishSubject.create() override val chipDeletedIntent: Subject by lazy { chipsAdapter.chipDeleted } override val menuReadyIntent: Observable = menu.map { Unit } override val optionsItemIntent: Subject = PublishSubject.create() @@ -116,6 +124,18 @@ class ComposeActivity : QkThemedActivity(), ComposeView { override val viewQksmsPlusIntent: Subject = PublishSubject.create() override val backPressedIntent: Subject = PublishSubject.create() + private val phoneNumberDialog by lazy { + QkDialog(this).apply { + titleRes = R.string.compose_number_picker_title + adapter = phoneNumberAdapter + positiveButton = R.string.compose_number_picker_always + positiveButtonListener = { phoneNumberActionIntent.onNext(PhoneNumberAction.ALWAYS) } + negativeButton = R.string.compose_number_picker_once + negativeButtonListener = { phoneNumberActionIntent.onNext(PhoneNumberAction.JUST_ONCE) } + cancelListener = { phoneNumberActionIntent.onNext(PhoneNumberAction.CANCEL) } + } + } + private val viewModel by lazy { ViewModelProviders.of(this, viewModelFactory)[ComposeViewModel::class.java] } private var cameraDestination: Uri? = null @@ -202,8 +222,10 @@ class ComposeActivity : QkThemedActivity(), ComposeView { if (state.editingMode && contacts.adapter == null) contacts.adapter = contactsAdapter toolbar.menu.findItem(R.id.add)?.isVisible = state.editingMode && !state.searching - toolbar.menu.findItem(R.id.call)?.isVisible = !state.editingMode && state.selectedMessages == 0 && state.query.isEmpty() - toolbar.menu.findItem(R.id.info)?.isVisible = !state.editingMode && state.selectedMessages == 0 && state.query.isEmpty() + toolbar.menu.findItem(R.id.call)?.isVisible = !state.editingMode && state.selectedMessages == 0 + && state.query.isEmpty() + toolbar.menu.findItem(R.id.info)?.isVisible = !state.editingMode && state.selectedMessages == 0 + && state.query.isEmpty() toolbar.menu.findItem(R.id.copy)?.isVisible = !state.editingMode && state.selectedMessages == 1 toolbar.menu.findItem(R.id.details)?.isVisible = !state.editingMode && state.selectedMessages == 1 toolbar.menu.findItem(R.id.delete)?.isVisible = !state.editingMode && state.selectedMessages > 0 @@ -219,6 +241,14 @@ class ComposeActivity : QkThemedActivity(), ComposeView { chipsAdapter.data = state.selectedChips contactsAdapter.data = state.composeItems + if (state.selectedContact != null && !phoneNumberDialog.isShowing) { + phoneNumberAdapter.data = state.selectedContact.numbers + phoneNumberDialog.subtitle = state.selectedContact.name + phoneNumberDialog.show() + } else if (state.selectedContact == null && phoneNumberDialog.isShowing) { + phoneNumberDialog.dismiss() + } + loading.setVisible(state.loading) sendAsGroup.setVisible(state.editingMode && state.selectedChips.size >= 2) @@ -282,7 +312,8 @@ class ComposeActivity : QkThemedActivity(), ComposeView { calendar.set(Calendar.HOUR_OF_DAY, hour) calendar.set(Calendar.MINUTE, minute) scheduleSelectedIntent.onNext(calendar.timeInMillis) - }, calendar.get(Calendar.HOUR_OF_DAY), calendar.get(Calendar.MINUTE), DateFormat.is24HourFormat(this)).show() + }, calendar.get(Calendar.HOUR_OF_DAY), calendar.get(Calendar.MINUTE), DateFormat.is24HourFormat(this)) + .show() }, calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH)).show() } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeState.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeState.kt index b2f55b0e..f7b7e7b8 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeState.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeState.kt @@ -22,6 +22,7 @@ import com.moez.QKSMS.compat.SubscriptionInfoCompat import com.moez.QKSMS.feature.compose.editing.Chip import com.moez.QKSMS.feature.compose.editing.ComposeItem import com.moez.QKSMS.model.Attachment +import com.moez.QKSMS.model.Contact import com.moez.QKSMS.model.Conversation import com.moez.QKSMS.model.Message import io.realm.RealmResults @@ -32,6 +33,7 @@ data class ComposeState( val searching: Boolean = false, val composeItems: List = ArrayList(), val selectedConversation: Long = 0, + val selectedContact: Contact? = null, // For phone number picker val selectedChips: List = ArrayList(), val sendAsGroup: Boolean = true, val conversationtitle: String = "", diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeView.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeView.kt index 78f1cbe8..4c4af90f 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeView.kt @@ -22,8 +22,10 @@ import android.net.Uri import androidx.annotation.StringRes import androidx.core.view.inputmethod.InputContentInfoCompat import com.moez.QKSMS.common.base.QkView +import com.moez.QKSMS.extensions.Optional import com.moez.QKSMS.feature.compose.editing.Chip import com.moez.QKSMS.feature.compose.editing.ComposeItem +import com.moez.QKSMS.feature.compose.editing.PhoneNumberAction import com.moez.QKSMS.model.Attachment import io.reactivex.Observable import io.reactivex.subjects.Subject @@ -34,7 +36,10 @@ interface ComposeView : QkView { val queryChangedIntent: Observable val queryBackspaceIntent: Observable<*> val queryEditorActionIntent: Observable - val chipSelectedIntent: Subject + val composeItemPressedIntent: Subject + val composeItemLongPressedIntent: Subject + val phoneNumberSelectedIntent: Subject> + val phoneNumberActionIntent: Subject val chipDeletedIntent: Subject val menuReadyIntent: Observable val optionsItemIntent: Observable diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeViewModel.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeViewModel.kt index d28fea7c..3d00fcdd 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeViewModel.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeViewModel.kt @@ -40,6 +40,7 @@ import com.moez.QKSMS.extensions.removeAccents import com.moez.QKSMS.feature.compose.editing.Chip import com.moez.QKSMS.feature.compose.editing.ComposeItem import com.moez.QKSMS.feature.compose.editing.ComposeItem.* +import com.moez.QKSMS.feature.compose.editing.PhoneNumberAction import com.moez.QKSMS.filter.ContactFilter import com.moez.QKSMS.filter.ContactGroupFilter import com.moez.QKSMS.interactor.AddScheduledMessage @@ -49,6 +50,7 @@ import com.moez.QKSMS.interactor.DeleteMessages import com.moez.QKSMS.interactor.MarkRead import com.moez.QKSMS.interactor.RetrySending import com.moez.QKSMS.interactor.SendMessage +import com.moez.QKSMS.interactor.SetDefaultPhoneNumber import com.moez.QKSMS.manager.ActiveConversationManager import com.moez.QKSMS.manager.PermissionManager import com.moez.QKSMS.model.Attachment @@ -77,6 +79,8 @@ import io.reactivex.subjects.BehaviorSubject import io.reactivex.subjects.PublishSubject import io.reactivex.subjects.Subject import io.realm.RealmList +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.rx2.awaitFirst import timber.log.Timber import java.util.* import javax.inject.Inject @@ -107,6 +111,7 @@ class ComposeViewModel @Inject constructor( private val prefs: Preferences, private val retrySending: RetrySending, private val sendMessage: SendMessage, + private val setDefaultPhoneNumber: SetDefaultPhoneNumber, private val subscriptionManager: SubscriptionManagerCompat, private val syncContacts: ContactSync ) : QkViewModel(ComposeState( @@ -310,18 +315,49 @@ class ComposeViewModel @Inject constructor( .autoDisposable(view.scope()) .subscribe() - // Enter the first contact suggestion if the enter button is pressed, and enter contacts that are selected + // Listen for ComposeItems being selected, and then send them off to the number picker dialog in case + // the user needs to select a phone number view.queryEditorActionIntent .filter { actionId -> actionId == EditorInfo.IME_ACTION_DONE } .withLatestFrom(state) { _, state -> state } .mapNotNull { state -> state.composeItems.firstOrNull() } - .mergeWith(view.chipSelectedIntent) + .mergeWith(view.composeItemPressedIntent) + .map { composeItem -> composeItem to false } + .mergeWith(view.composeItemLongPressedIntent.map { composeItem -> composeItem to true }) + .observeOn(Schedulers.io()) + .skipUntil(state.filter { state -> state.editingMode }) + .takeUntil(state.filter { state -> !state.editingMode }) .autoDisposable(view.scope()) - .subscribe { composeItem -> - newState { copy(searching = false) } - chipsReducer.onNext { chips -> - chips + composeItem.getContacts().map { Chip(it.numbers.first()?.address.orEmpty(), it) } + .subscribe { (composeItem, force) -> + val contacts = composeItem.getContacts() + val newChips = contacts.map { contact -> + if (contact.numbers.size == 1 || contact.getDefaultNumber() != null && !force) { + val number = contact.getDefaultNumber() ?: contact.numbers[0]!! + Chip(number.address, contact) + } else { + runBlocking { + newState { copy(selectedContact = contact) } + val action = view.phoneNumberActionIntent.awaitFirst() + newState { copy(selectedContact = null) } + val numberId = view.phoneNumberSelectedIntent.awaitFirst().value + val number = contact.numbers.find { number -> number.id == numberId } + + if (action == PhoneNumberAction.CANCEL || number == null) { + return@runBlocking null + } + + if (action == PhoneNumberAction.ALWAYS) { + val params = SetDefaultPhoneNumber.Params(contact.lookupKey, number.id) + setDefaultPhoneNumber.execute(params) + } + + Chip(number.address, contact) + } ?: return@subscribe + } } + + newState { copy(searching = false) } + chipsReducer.onNext { chips -> chips + newChips } } // Update the list of selected contacts when a new contact is selected or an existing one is deselected diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ComposeItemAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ComposeItemAdapter.kt index 8a4a4dcb..b411b214 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ComposeItemAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ComposeItemAdapter.kt @@ -40,7 +40,8 @@ import javax.inject.Inject class ComposeItemAdapter @Inject constructor(private val colors: Colors) : QkAdapter() { - val itemSelected: Subject = PublishSubject.create() + val clicks: Subject = PublishSubject.create() + val longClicks: Subject = PublishSubject.create() private val numbersViewPool = RecyclerView.RecycledViewPool() @@ -57,7 +58,12 @@ class ComposeItemAdapter @Inject constructor(private val colors: Colors) : QkAda return QkViewHolder(view).apply { view.setOnClickListener { val item = getItem(adapterPosition) - itemSelected.onNext(item) + clicks.onNext(item) + } + view.setOnLongClickListener { + val item = getItem(adapterPosition) + longClicks.onNext(item) + true } } } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/PhoneNumberAction.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/PhoneNumberAction.kt new file mode 100644 index 00000000..7464cefd --- /dev/null +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/PhoneNumberAction.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2019 Moez Bhatti + * + * This file is part of QKSMS. + * + * QKSMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QKSMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with QKSMS. If not, see . + */ +package com.moez.QKSMS.feature.compose.editing + +enum class PhoneNumberAction { + CANCEL, + JUST_ONCE, + ALWAYS +} diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/PhoneNumberPickerAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/PhoneNumberPickerAdapter.kt new file mode 100644 index 00000000..3464241f --- /dev/null +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/PhoneNumberPickerAdapter.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2019 Moez Bhatti + * + * This file is part of QKSMS. + * + * QKSMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * QKSMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with QKSMS. If not, see . + */ +package com.moez.QKSMS.feature.compose.editing + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import com.moez.QKSMS.R +import com.moez.QKSMS.common.base.QkAdapter +import com.moez.QKSMS.common.base.QkViewHolder +import com.moez.QKSMS.extensions.Optional +import com.moez.QKSMS.model.PhoneNumber +import io.reactivex.subjects.BehaviorSubject +import io.reactivex.subjects.Subject +import kotlinx.android.synthetic.main.phone_number_list_item.view.* +import kotlinx.android.synthetic.main.radio_preference_view.view.* +import javax.inject.Inject + +class PhoneNumberPickerAdapter @Inject constructor( + private val context: Context +) : QkAdapter() { + + val selectedItemChanges: Subject> = BehaviorSubject.create() + + private var selectedItem: Long? = null + set(value) { + data.indexOfFirst { number -> number.id == field }.takeIf { it != -1 }?.run(::notifyItemChanged) + field = value + data.indexOfFirst { number -> number.id == field }.takeIf { it != -1 }?.run(::notifyItemChanged) + selectedItemChanges.onNext(Optional(value)) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QkViewHolder { + val inflater = LayoutInflater.from(parent.context) + val view = inflater.inflate(R.layout.phone_number_list_item, parent, false) + return QkViewHolder(view).apply { + view.setOnClickListener { + val phoneNumber = getItem(adapterPosition) + selectedItem = phoneNumber.id + } + } + } + + override fun onBindViewHolder(holder: QkViewHolder, position: Int) { + val phoneNumber = getItem(position) + val view = holder.itemView + + view.number.radioButton.isChecked = phoneNumber.id == selectedItem + view.number.titleView.text = phoneNumber.address + view.number.summaryView.text = when (phoneNumber.isDefault) { + true -> context.getString(R.string.compose_number_picker_default, phoneNumber.type) + false -> phoneNumber.type + } + } + + override fun onDatasetChanged() { + super.onDatasetChanged() + selectedItem = data.find { number -> number.isDefault }?.id ?: data.firstOrNull()?.id + } + +} diff --git a/presentation/src/main/res/layout/phone_number_list_item.xml b/presentation/src/main/res/layout/phone_number_list_item.xml new file mode 100644 index 00000000..c5a85048 --- /dev/null +++ b/presentation/src/main/res/layout/phone_number_list_item.xml @@ -0,0 +1,33 @@ + + + + + + + diff --git a/presentation/src/main/res/layout/qk_dialog.xml b/presentation/src/main/res/layout/qk_dialog.xml new file mode 100644 index 00000000..f38decc2 --- /dev/null +++ b/presentation/src/main/res/layout/qk_dialog.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index ac18e927..01be513b 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -98,6 +98,10 @@ Delete + Choose a phone number + %s ∙ Default + Just once + Always %d selected %1$d of %2$d results Send as group message