Skip to content

Commit

Permalink
Build phone number picker
Browse files Browse the repository at this point in the history
  • Loading branch information
moezbhatti committed Dec 27, 2019
1 parent 058d12d commit bb81620
Show file tree
Hide file tree
Showing 21 changed files with 531 additions and 28 deletions.
1 change: 0 additions & 1 deletion data/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
19 changes: 11 additions & 8 deletions data/src/main/java/com/moez/QKSMS/mapper/CursorToContactImpl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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++
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -286,6 +286,13 @@ class SyncRepositoryImpl @Inject constructor(
}

private fun getContacts(): List<Contact> {
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 }
Expand All @@ -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
}
Expand Down
1 change: 0 additions & 1 deletion domain/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright (C) 2019 Moez Bhatti <[email protected]>
*
* 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.interactor

import com.moez.QKSMS.repository.ContactRepository
import io.reactivex.Flowable
import javax.inject.Inject

class SetDefaultPhoneNumber @Inject constructor(
private val contactRepo: ContactRepository
) : Interactor<SetDefaultPhoneNumber.Params>() {

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)
}
}

}
6 changes: 5 additions & 1 deletion domain/src/main/java/com/moez/QKSMS/model/Contact.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,8 @@ open class Contact(
var name: String = "",
var starred: Boolean = false,
var lastUpdate: Long = 0
) : RealmObject()
) : RealmObject() {

fun getDefaultNumber(): PhoneNumber? = numbers.find { number -> number.isDefault }

}
7 changes: 5 additions & 2 deletions domain/src/main/java/com/moez/QKSMS/model/PhoneNumber.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
var type: String = "",
var isDefault: Boolean = false
) : RealmObject()
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,6 @@ interface ContactRepository {

fun getUnmanagedContactGroups(): Observable<List<ContactGroup>>

}
fun setDefaultPhoneNumber(lookupKey: String, phoneNumberId: Long)

}
105 changes: 105 additions & 0 deletions presentation/src/main/java/com/moez/QKSMS/common/widget/QkDialog.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* Copyright (C) 2019 Moez Bhatti <[email protected]>
*
* 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.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)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<Boolean> = PublishSubject.create()
override val queryChangedIntent: Observable<CharSequence> by lazy { search.textChanges() }
override val queryBackspaceIntent: Observable<*> by lazy { search.backspaces }
override val queryEditorActionIntent: Observable<Int> by lazy { search.editorActions() }
override val chipSelectedIntent: Subject<ComposeItem> by lazy { contactsAdapter.itemSelected }
override val composeItemPressedIntent: Subject<ComposeItem> by lazy { contactsAdapter.clicks }
override val composeItemLongPressedIntent: Subject<ComposeItem> by lazy { contactsAdapter.longClicks }
override val phoneNumberSelectedIntent: Subject<Optional<Long>> by lazy { phoneNumberAdapter.selectedItemChanges }
override val phoneNumberActionIntent: Subject<PhoneNumberAction> = PublishSubject.create()
override val chipDeletedIntent: Subject<Chip> by lazy { chipsAdapter.chipDeleted }
override val menuReadyIntent: Observable<Unit> = menu.map { Unit }
override val optionsItemIntent: Subject<Int> = PublishSubject.create()
Expand All @@ -116,6 +124,18 @@ class ComposeActivity : QkThemedActivity(), ComposeView {
override val viewQksmsPlusIntent: Subject<Unit> = PublishSubject.create()
override val backPressedIntent: Subject<Unit> = 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
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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()
}

Expand Down
Loading

0 comments on commit bb81620

Please sign in to comment.