Flatten hierarchy in presentation module

This commit is contained in:
Moez Bhatti
2018-03-22 01:43:09 -04:00
parent 2e4d96059a
commit 7212983ffe
95 changed files with 51 additions and 65 deletions
@@ -0,0 +1,38 @@
/*
* 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 presentation.common
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target
class GlideCompletionListener<T>(private val listener: () -> Unit) : RequestListener<T> {
override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<T>?, isFirstResource: Boolean): Boolean {
listener()
return false
}
override fun onResourceReady(resource: T, model: Any?, target: Target<T>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean {
listener()
return false
}
}
@@ -0,0 +1,90 @@
/*
* 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 presentation.common
import android.content.Context
import android.support.annotation.ArrayRes
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.ViewGroup
import com.jakewharton.rxbinding2.view.clicks
import com.moez.QKSMS.R
import common.util.Colors
import common.util.extensions.setTint
import common.util.extensions.setVisible
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.rxkotlin.plusAssign
import io.reactivex.subjects.PublishSubject
import io.reactivex.subjects.Subject
import kotlinx.android.synthetic.main.menu_list_item.view.*
import presentation.common.base.QkAdapter
import presentation.common.base.QkViewHolder
import javax.inject.Inject
data class MenuItem(val title: String, val actionId: Int)
class MenuItemAdapter @Inject constructor(private val context: Context, private val colors: Colors) : QkAdapter<MenuItem>() {
val menuItemClicks: Subject<Int> = PublishSubject.create()
private val disposables = CompositeDisposable()
var selectedItem: Int? = null
set(value) {
val old = data.map { it.actionId }.indexOfFirst { it == field }
val new = data.map { it.actionId }.indexOfFirst { it == value }
field = value
old.let { notifyItemChanged(it) }
new.let { notifyItemChanged(it) }
}
fun setData(@ArrayRes titles: Int, @ArrayRes values: Int = -1) {
val valueInts = if (values != -1) context.resources.getIntArray(values) else null
data = context.resources.getStringArray(titles)
.mapIndexed { index, title -> MenuItem(title, valueInts?.getOrNull(index) ?: index) }
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QkViewHolder {
val view = LayoutInflater.from(context).inflate(R.layout.menu_list_item, parent, false)
disposables += colors.theme
.subscribe { view.check.setTint(it) }
return QkViewHolder(view)
}
override fun onBindViewHolder(holder: QkViewHolder, position: Int) {
val menuItem = getItem(position)
val view = holder.itemView
view.clicks().subscribe { menuItemClicks.onNext(menuItem.actionId) }
view.title.text = menuItem.title
view.check.setVisible(menuItem.actionId == selectedItem)
}
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
super.onDetachedFromRecyclerView(recyclerView)
disposables.clear()
}
}
@@ -0,0 +1,198 @@
/*
* 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 presentation.common
import android.app.Activity
import android.app.ActivityOptions
import android.arch.lifecycle.ViewModel
import android.arch.lifecycle.ViewModelProvider
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.ContactsContract
import android.provider.Settings
import android.provider.Telephony
import android.view.View
import manager.NotificationManager
import presentation.feature.blocked.BlockedActivity
import presentation.feature.blocked.BlockedViewModel
import presentation.feature.compose.ComposeActivity
import presentation.feature.compose.ComposeViewModel
import presentation.feature.conversationinfo.ConversationInfoActivity
import presentation.feature.conversationinfo.ConversationInfoViewModel
import presentation.feature.gallery.GalleryActivity
import presentation.feature.gallery.GalleryViewModel
import presentation.feature.main.MainViewModel
import presentation.feature.notificationprefs.NotificationPrefsActivity
import presentation.feature.notificationprefs.NotificationPrefsViewModel
import presentation.feature.plus.PlusActivity
import presentation.feature.plus.PlusViewModel
import presentation.feature.settings.SettingsActivity
import presentation.feature.settings.SettingsViewModel
import presentation.feature.setup.SetupActivity
import presentation.feature.setup.SetupViewModel
import presentation.feature.themepicker.ThemePickerActivity
import presentation.feature.themepicker.ThemePickerViewModel
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class Navigator @Inject constructor(private val context: Context, private val notificationManager: NotificationManager) {
private fun startActivity(intent: Intent) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
}
private fun startActivityExternal(intent: Intent) {
if (intent.resolveActivity(context.packageManager) != null) {
startActivity(intent)
} else {
startActivity(Intent.createChooser(intent, null))
}
}
fun showSetupActivity() {
val intent = Intent(context, SetupActivity::class.java)
startActivity(intent)
}
fun showQksmsPlusActivity() {
val intent = Intent(context, PlusActivity::class.java)
startActivity(intent)
}
fun showDefaultSmsDialog() {
val intent = Intent(Telephony.Sms.Intents.ACTION_CHANGE_DEFAULT)
if (Telephony.Sms.getDefaultSmsPackage(context) != context.packageName) {
intent.putExtra(Telephony.Sms.Intents.EXTRA_PACKAGE_NAME, context.packageName)
}
startActivity(intent)
}
fun showCompose(body: String? = null) {
val intent = Intent(context, ComposeActivity::class.java)
intent.putExtra(Intent.EXTRA_TEXT, body)
startActivity(intent)
}
fun showConversation(threadId: Long) {
val intent = Intent(context, ComposeActivity::class.java)
intent.putExtra("threadId", threadId)
startActivity(intent)
}
fun showConversationInfo(threadId: Long) {
val intent = Intent(context, ConversationInfoActivity::class.java)
intent.putExtra("threadId", threadId)
startActivity(intent)
}
fun showImage(partId: Long) {
val intent = Intent(context, GalleryActivity::class.java)
intent.putExtra("partId", partId)
startActivity(intent)
}
/**
* Shows the attachment full-screen
* The transitionName for the view should be the id of the image being displayed
*/
fun showImageAnimated(from: Activity, view: View) {
val intent = Intent(context, GalleryActivity::class.java)
intent.putExtra("partId", view.transitionName.toLong())
val options = ActivityOptions.makeSceneTransitionAnimation(from, view, view.transitionName)
from.startActivity(intent, options.toBundle())
}
fun showSettings() {
val intent = Intent(context, SettingsActivity::class.java)
startActivity(intent)
}
fun showBlockedConversations() {
val intent = Intent(context, BlockedActivity::class.java)
startActivity(intent)
}
fun showThemePicker(threadId: Long = 0) {
val intent = Intent(context, ThemePickerActivity::class.java)
intent.putExtra("threadId", threadId)
startActivity(intent)
}
fun makePhoneCall(address: String) {
val intent = Intent(Intent.ACTION_DIAL, Uri.parse("tel:$address"))
startActivityExternal(intent)
}
fun showSupport() {
val intent = Intent(Intent.ACTION_SENDTO)
intent.data = Uri.parse("mailto:")
intent.putExtra(Intent.EXTRA_EMAIL, arrayOf("team@qklabs.com"))
startActivityExternal(intent)
}
fun addContact(address: String) {
val uri = Uri.parse("tel: $address")
val intent = Intent(ContactsContract.Intents.SHOW_OR_CREATE_CONTACT, uri)
startActivityExternal(intent)
}
fun showNotificationSettings(threadId: Long = 0) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (threadId != 0L) {
notificationManager.createNotificationChannel(threadId)
}
val channelId = notificationManager.buildNotificationChannelId(threadId)
val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
intent.putExtra(Settings.EXTRA_CHANNEL_ID, channelId)
intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
startActivity(intent)
} else {
val intent = Intent(context, NotificationPrefsActivity::class.java)
intent.putExtra("threadId", threadId)
startActivity(intent)
}
}
class ViewModelFactory(private val intent: Intent) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return when (modelClass) {
MainViewModel::class.java -> MainViewModel()
PlusViewModel::class.java -> PlusViewModel()
SetupViewModel::class.java -> SetupViewModel()
ComposeViewModel::class.java -> ComposeViewModel(intent)
ConversationInfoViewModel::class.java -> ConversationInfoViewModel(intent)
GalleryViewModel::class.java -> GalleryViewModel(intent)
NotificationPrefsViewModel::class.java -> NotificationPrefsViewModel(intent)
SettingsViewModel::class.java -> SettingsViewModel()
BlockedViewModel::class.java -> BlockedViewModel()
ThemePickerViewModel::class.java -> ThemePickerViewModel(intent)
else -> throw IllegalArgumentException("Invalid ViewModel class. If this is a new ViewModel, please add it to Navigator.kt")
} as T
}
}
}
@@ -1,4 +1,4 @@
/*
package common/*
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
*
* This file is part of QKSMS.
@@ -16,8 +16,6 @@
* You should have received a copy of the GNU General Public License
* along with QKSMS. If not, see <http://www.gnu.org/licenses/>.
*/
package common
import android.app.Application
import android.support.text.emoji.EmojiCompat
import android.support.text.emoji.FontRequestEmojiCompatConfig
@@ -25,15 +23,14 @@ import android.support.v4.provider.FontRequest
import com.bugsnag.android.Bugsnag
import com.moez.QKSMS.BuildConfig
import com.moez.QKSMS.R
import common.di.AppComponentManager
import common.di.appComponent
import manager.AnalyticsManager
import injection.AppComponentManager
import injection.appComponent
import io.realm.Realm
import io.realm.RealmConfiguration
import manager.AnalyticsManager
import timber.log.Timber
import javax.inject.Inject
class QKApplication : Application() {
/**
@@ -0,0 +1,79 @@
/*
* 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 presentation.common.base
import android.support.annotation.CallSuper
import android.support.v7.widget.RecyclerView
import io.reactivex.Flowable
import io.reactivex.disposables.Disposable
/**
* Base RecyclerView.Adapter that provides some convenience when creating a new Adapter, such as
* data list handing and item animations
*/
abstract class FlowableAdapter<T> : QkAdapter<T>() {
var flowable: Flowable<List<T>>? = null
set(value) {
if (field == value) return
field = value
// Stop listening for updates on the old flowable
dispose()
// If we're attached to any RecyclerViews, then subscribe to updates
if (recyclerViews.isNotEmpty()) {
subscribe()
}
}
private val recyclerViews = ArrayList<RecyclerView>()
private var disposable: Disposable? = null
@CallSuper
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
// If this is the first RecyclerView to be attached, then start listening to updates
if (recyclerViews.isEmpty() || disposable == null) {
subscribe()
}
recyclerViews.add(recyclerView)
}
@CallSuper
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
recyclerViews.remove(recyclerView)
// If no more RecyclerViews are attached, stop listening to updates
if (recyclerViews.isEmpty()) {
dispose()
}
}
private fun subscribe() {
disposable = flowable?.subscribe { data = it }
}
private fun dispose() {
disposable?.dispose()
}
}
@@ -0,0 +1,77 @@
/*
* 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 presentation.common.base
import android.annotation.SuppressLint
import android.arch.lifecycle.ViewModelProviders
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.view.Menu
import android.view.MenuItem
import io.reactivex.subjects.BehaviorSubject
import io.reactivex.subjects.Subject
import kotlinx.android.synthetic.main.toolbar.*
import presentation.common.Navigator
import kotlin.reflect.KClass
abstract class QkActivity<VM : QkViewModel<*, *>> : AppCompatActivity() {
protected abstract val viewModelClass: KClass<VM>
protected val viewModel: VM by lazy {
ViewModelProviders.of(this, Navigator.ViewModelFactory(intent))[viewModelClass.java]
}
protected val menu: Subject<Menu> = BehaviorSubject.create()
@SuppressLint("InlinedApi")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
onNewIntent(intent)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
onBackPressed()
true
}
else -> super.onOptionsItemSelected(item)
}
}
override fun setContentView(layoutResID: Int) {
super.setContentView(layoutResID)
setSupportActionBar(toolbar)
title = title // The title may have been set before layout inflation
}
override fun setTitle(title: CharSequence?) {
super.setTitle(title)
toolbarTitle?.text = title
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
val result = super.onCreateOptionsMenu(menu)
if (menu != null) {
this.menu.onNext(menu)
}
return result
}
}
@@ -0,0 +1,74 @@
/*
* 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 presentation.common.base
import android.support.v7.util.DiffUtil
import android.support.v7.widget.RecyclerView
/**
* Base RecyclerView.Adapter that provides some convenience when creating a new Adapter, such as
* data list handing and item animations
*/
abstract class QkAdapter<T> : RecyclerView.Adapter<QkViewHolder>() {
var data: List<T> = ArrayList()
set(value) {
val diff = DiffUtil.calculateDiff(getDiffUtilCallback(field, value))
field = value
diff.dispatchUpdatesTo(this)
onDatasetChanged()
}
fun getItem(position: Int): T {
return data[position]
}
override fun getItemCount(): Int {
return data.size
}
open fun onDatasetChanged() {}
/**
* Allows the adapter implementation to provide a custom DiffUtil.Callback
* If not, then the abstract implementation will be used
*/
private fun getDiffUtilCallback(oldData: List<T>, newData: List<T>): DiffUtil.Callback {
return object : DiffUtil.Callback() {
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
areItemsTheSame(oldData[oldItemPosition], newData[newItemPosition])
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
areContentsTheSame(oldData[oldItemPosition], newData[newItemPosition])
override fun getOldListSize() = oldData.size
override fun getNewListSize() = newData.size
}
}
open protected fun areItemsTheSame(old: T, new: T): Boolean {
return old == new
}
open protected fun areContentsTheSame(old: T, new: T): Boolean {
return old == new
}
}
@@ -0,0 +1,107 @@
/*
* 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 presentation.common.base
import android.annotation.SuppressLint
import android.arch.lifecycle.Lifecycle
import android.os.Bundle
import com.moez.QKSMS.R
import com.uber.autodispose.android.lifecycle.scope
import com.uber.autodispose.kotlin.autoDisposable
import common.util.Colors
import io.reactivex.rxkotlin.Observables
import io.reactivex.subjects.BehaviorSubject
import io.reactivex.subjects.Subject
import kotlinx.android.synthetic.main.toolbar.*
import javax.inject.Inject
/**
* Base activity that automatically applies any necessary theme theme settings and colors
*
* In most cases, this should be used instead of the base QkActivity, except for when
* an activity does not depend on the theme
*/
abstract class QkThemedActivity<VM : QkViewModel<*, *>> : QkActivity<VM>() {
@Inject lateinit var colors: Colors
/**
* In case the activity should be themed for a specific conversation, the selected conversation
* can be changed by pushing the threadId to this subject
*/
protected val threadId: Subject<Long> = BehaviorSubject.createDefault(0)
/**
* Switch the theme if the threadId changes
*/
protected val theme = threadId
.distinctUntilChanged()
.switchMap { threadId -> colors.themeForConversation(threadId) }
@SuppressLint("InlinedApi")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
colors.appThemeResources
.autoDisposable(scope())
.subscribe { res -> setTheme(res) }
colors.statusBarIcons
.autoDisposable(scope())
.subscribe { systemUiVisibility -> window.decorView.systemUiVisibility = systemUiVisibility }
colors.statusBar
.autoDisposable(scope())
.subscribe { color -> window.statusBarColor = color }
}
override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState)
// Update the colours of the menu items
Observables.combineLatest(menu, theme, colors.textTertiary, { menu, theme, textTertiary ->
(0 until menu.size())
.map { position -> menu.getItem(position) }
.forEach { menuItem ->
menuItem?.icon?.run {
setTint(when (menuItem.itemId) {
R.id.info -> textTertiary
else -> theme
})
menuItem.icon = this
}
}
}).autoDisposable(scope(Lifecycle.Event.ON_DESTROY)).subscribe()
colors.textTertiary
.doOnNext { color -> toolbar?.overflowIcon = toolbar?.overflowIcon?.apply { setTint(color) } }
.doOnNext { color -> toolbar?.navigationIcon = toolbar?.navigationIcon?.apply { setTint(color) } }
.autoDisposable(scope(Lifecycle.Event.ON_DESTROY))
.subscribe()
colors.popupThemeResource
.autoDisposable(scope(Lifecycle.Event.ON_DESTROY))
.subscribe { res -> toolbar?.popupTheme = res }
colors.toolbarColor
.autoDisposable(scope(Lifecycle.Event.ON_DESTROY))
.subscribe { color -> toolbar?.setBackgroundColor(color) }
}
}
@@ -16,19 +16,12 @@
* You should have received a copy of the GNU General Public License
* along with QKSMS. If not, see <http://www.gnu.org/licenses/>.
*/
package common.di
package presentation.common.base
import common.QKApplication
import android.arch.lifecycle.LifecycleOwner
internal lateinit var appComponent: AppComponent
private set
interface QkView<in State> : LifecycleOwner {
internal object AppComponentManager {
fun init(application: QKApplication) {
appComponent = DaggerAppComponent.builder()
.appModule(AppModule(application))
.build()
}
fun render(state: State)
}
@@ -0,0 +1,24 @@
/*
* 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 presentation.common.base
import android.support.v7.widget.RecyclerView
import android.view.View
class QkViewHolder(view: View) : RecyclerView.ViewHolder(view)
@@ -0,0 +1,52 @@
/*
* 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 presentation.common.base
import android.arch.lifecycle.ViewModel
import android.support.annotation.CallSuper
import com.uber.autodispose.android.lifecycle.scope
import com.uber.autodispose.kotlin.autoDisposable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.subjects.BehaviorSubject
abstract class QkViewModel<in View : QkView<State>, State>(initialState: State) : ViewModel() {
protected val state: BehaviorSubject<State> = BehaviorSubject.createDefault(initialState)
protected val disposables = CompositeDisposable()
@CallSuper
open fun bindView(view: View) {
state
.observeOn(AndroidSchedulers.mainThread())
.autoDisposable(view.scope())
.subscribe { view.render(it) }
}
protected fun newState(reducer: (State) -> State) {
state.value?.let { state.onNext(reducer(it)) }
}
override fun onCleared() {
super.onCleared()
disposables.dispose()
}
}
@@ -1,120 +0,0 @@
/*
* 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 common.di
import common.QKApplication
import common.util.ContactImageLoader
import dagger.Component
import presentation.common.widget.AvatarView
import presentation.common.widget.MmsPreviewView
import presentation.common.widget.PagerTitleView
import presentation.common.widget.PreferenceView
import presentation.common.widget.QkEditText
import presentation.common.widget.QkSwitch
import presentation.common.widget.QkTextView
import presentation.common.widget.Separator
import presentation.feature.blocked.BlockedActivity
import presentation.feature.blocked.BlockedViewModel
import presentation.feature.compose.ComposeActivity
import presentation.feature.compose.ComposeViewModel
import presentation.feature.compose.DetailedChipView
import presentation.feature.conversationinfo.ConversationInfoActivity
import presentation.feature.conversationinfo.ConversationInfoViewModel
import presentation.feature.gallery.GalleryActivity
import presentation.feature.gallery.GalleryViewModel
import presentation.feature.main.MainActivity
import presentation.feature.main.MainViewModel
import presentation.feature.notificationprefs.NotificationPrefsActivity
import presentation.feature.notificationprefs.NotificationPrefsViewModel
import presentation.feature.plus.PlusActivity
import presentation.feature.plus.PlusViewModel
import presentation.feature.settings.SettingsActivity
import presentation.feature.settings.SettingsViewModel
import presentation.feature.setup.SetupActivity
import presentation.feature.setup.SetupViewModel
import presentation.feature.themepicker.ThemePickerActivity
import presentation.feature.themepicker.ThemePickerViewModel
import presentation.receiver.DefaultSmsChangedReceiver
import presentation.receiver.MarkReadReceiver
import presentation.receiver.MarkSeenReceiver
import presentation.receiver.MmsReceivedReceiver
import presentation.receiver.MmsSentReceiver
import presentation.receiver.MmsUpdatedReceiver
import presentation.receiver.NightModeReceiver
import presentation.receiver.RemoteMessagingReceiver
import presentation.receiver.SmsDeliveredReceiver
import presentation.receiver.SmsProviderChangedReceiver
import presentation.receiver.SmsReceiver
import presentation.receiver.SmsSentReceiver
import javax.inject.Singleton
@Singleton
@Component(modules = [(AppModule::class)])
interface AppComponent {
fun inject(application: QKApplication)
fun inject(activity: MainActivity)
fun inject(activity: SetupActivity)
fun inject(activity: BlockedActivity)
fun inject(activity: ComposeActivity)
fun inject(activity: ConversationInfoActivity)
fun inject(activity: GalleryActivity)
fun inject(activity: NotificationPrefsActivity)
fun inject(activity: PlusActivity)
fun inject(activity: SettingsActivity)
fun inject(activity: ThemePickerActivity)
fun inject(receiver: DefaultSmsChangedReceiver)
fun inject(receiver: SmsDeliveredReceiver)
fun inject(receiver: SmsSentReceiver)
fun inject(receiver: MarkSeenReceiver)
fun inject(receiver: MarkReadReceiver)
fun inject(receiver: MmsReceivedReceiver)
fun inject(receiver: MmsSentReceiver)
fun inject(receiver: MmsUpdatedReceiver)
fun inject(receiver: NightModeReceiver)
fun inject(receiver: RemoteMessagingReceiver)
fun inject(receiver: SmsProviderChangedReceiver)
fun inject(receiver: SmsReceiver)
fun inject(view: AvatarView)
fun inject(view: DetailedChipView)
fun inject(view: MmsPreviewView)
fun inject(view: PagerTitleView)
fun inject(view: PreferenceView)
fun inject(view: QkEditText)
fun inject(view: QkSwitch)
fun inject(view: QkTextView)
fun inject(view: Separator)
fun inject(viewModel: MainViewModel)
fun inject(viewModel: SetupViewModel)
fun inject(viewModel: BlockedViewModel)
fun inject(viewModel: ComposeViewModel)
fun inject(viewModel: ConversationInfoViewModel)
fun inject(viewModel: GalleryViewModel)
fun inject(viewModel: NotificationPrefsViewModel)
fun inject(viewModel: PlusViewModel)
fun inject(viewModel: SettingsViewModel)
fun inject(viewModel: ThemePickerViewModel)
fun inject(fetcher: ContactImageLoader.ContactImageFetcher)
}
@@ -1,118 +0,0 @@
/*
* 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 common.di
import android.app.Application
import android.content.Context
import android.preference.PreferenceManager
import com.f2prateek.rx.preferences2.RxSharedPreferences
import common.util.AnalyticsManagerImpl
import common.util.NotificationManagerImpl
import dagger.Module
import dagger.Provides
import manager.AnalyticsManager
import manager.KeyManager
import manager.KeyManagerImpl
import manager.NotificationManager
import manager.PermissionManager
import manager.PermissionManagerImpl
import manager.WidgetManager
import manager.WidgetManagerImpl
import mapper.CursorToContact
import mapper.CursorToContactImpl
import mapper.CursorToConversation
import mapper.CursorToConversationImpl
import mapper.CursorToMessage
import mapper.CursorToMessageImpl
import mapper.CursorToPart
import mapper.CursorToPartImpl
import mapper.CursorToRecipienImpl
import mapper.CursorToRecipient
import repository.ContactRepository
import repository.ContactRepositoryImpl
import repository.MessageRepository
import repository.MessageRepositoryImpl
import repository.SyncRepository
import repository.SyncRepositoryImpl
import javax.inject.Singleton
@Module
class AppModule(private var application: Application) {
@Provides
@Singleton
fun provideContext(): Context {
return application
}
@Provides
@Singleton
fun provideRxPreferences(context: Context): RxSharedPreferences {
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
return RxSharedPreferences.create(preferences)
}
// Manager
@Provides
fun provideAnalyticsManager(manager: AnalyticsManagerImpl): AnalyticsManager = manager
@Provides
fun provideKeyManager(manager: KeyManagerImpl): KeyManager = manager
@Provides
fun provideNotificationsManager(manager: NotificationManagerImpl): NotificationManager = manager
@Provides
fun providePermissionsManager(manager: PermissionManagerImpl): PermissionManager = manager
@Provides
fun provideWidgetManager(manager: WidgetManagerImpl): WidgetManager = manager
// Mapper
@Provides
fun provideCursorToContact(mapper: CursorToContactImpl): CursorToContact = mapper
@Provides
fun provideCursorToConversation(mapper: CursorToConversationImpl): CursorToConversation = mapper
@Provides
fun provideCursorToMessage(mapper: CursorToMessageImpl): CursorToMessage = mapper
@Provides
fun provideCursorToPart(mapper: CursorToPartImpl): CursorToPart = mapper
@Provides
fun provideCursorToRecipient(mapper: CursorToRecipienImpl): CursorToRecipient = mapper
// Repository
@Provides
fun provideContactRepository(repository: ContactRepositoryImpl): ContactRepository = repository
@Provides
fun provideMessageRepository(repository: MessageRepositoryImpl): MessageRepository = repository
@Provides
fun provideSyncRepository(repository: SyncRepositoryImpl): SyncRepository = repository
}
@@ -29,7 +29,7 @@ import com.bumptech.glide.load.data.DataFetcher
import com.bumptech.glide.load.model.ModelLoader
import com.bumptech.glide.load.model.ModelLoaderFactory
import com.bumptech.glide.load.model.MultiModelLoaderFactory
import common.di.appComponent
import injection.appComponent
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import repository.ContactRepository
@@ -0,0 +1,119 @@
/*
* 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 presentation.common.widget
import android.content.Context
import android.net.Uri
import android.provider.ContactsContract
import android.telephony.PhoneNumberUtils
import android.util.AttributeSet
import android.view.View
import android.widget.FrameLayout
import com.jakewharton.rxbinding2.view.clicks
import com.moez.QKSMS.R
import com.uber.autodispose.android.scope
import com.uber.autodispose.kotlin.autoDisposable
import injection.appComponent
import common.util.Colors
import common.util.GlideApp
import common.util.extensions.setTint
import kotlinx.android.synthetic.main.avatar_view.view.*
import model.Contact
import presentation.common.Navigator
import javax.inject.Inject
class AvatarView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : FrameLayout(context, attrs) {
@Inject lateinit var colors: Colors
@Inject lateinit var navigator: Navigator
/**
* This value can be changes if we should use the theme from a particular conversation
*/
var threadId: Long = 0
var contact: Contact? = null
set(value) {
field = value
updateView()
}
init {
if (!isInEditMode) {
appComponent.inject(this)
}
View.inflate(context, R.layout.avatar_view, this)
setBackgroundResource(R.drawable.circle)
clipToOutline = true
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
if (!isInEditMode) {
colors.themeForConversation(threadId)
.autoDisposable(scope())
.subscribe { color -> background.setTint(color) }
colors.textPrimaryOnTheme
.autoDisposable(scope())
.subscribe { color -> icon.setTint(color) }
clicks()
.autoDisposable(scope())
.subscribe {
if (contact?.lookupKey.isNullOrEmpty()) {
contact?.numbers?.firstOrNull()?.let { number ->
navigator.addContact(number.address)
}
} else {
val key = contact?.lookupKey
val uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_LOOKUP_URI, key)
ContactsContract.QuickContact.showQuickContact(context, this@AvatarView, uri,
ContactsContract.QuickContact.MODE_MEDIUM, null)
}
}
}
}
override fun onFinishInflate() {
super.onFinishInflate()
if (!isInEditMode) {
updateView()
}
}
private fun updateView() {
if (contact?.name.orEmpty().isNotEmpty()) {
initial.text = contact?.name?.substring(0, 1)
icon.visibility = GONE
} else {
initial.text = null
icon.visibility = VISIBLE
}
photo.setImageDrawable(null)
contact?.numbers?.firstOrNull()?.address?.let { address ->
GlideApp.with(photo).load(PhoneNumberUtils.stripSeparators(address)).into(photo)
}
}
}
@@ -0,0 +1,64 @@
/*
* 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 presentation.common.widget
import android.content.Context
import android.support.constraint.ConstraintLayout
import android.util.AttributeSet
import android.view.View
import com.moez.QKSMS.R
import kotlinx.android.synthetic.main.group_avatar_view.view.*
import model.Contact
class GroupAvatarView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : ConstraintLayout(context, attrs) {
var contacts: List<Contact> = ArrayList()
set(value) {
field = value
updateView()
}
private val avatars by lazy { listOf(avatar1, avatar2, avatar3) }
init {
View.inflate(context, R.layout.group_avatar_view, this)
setBackgroundResource(R.drawable.circle)
clipToOutline = true
}
override fun onFinishInflate() {
super.onFinishInflate()
avatars.forEach { avatar ->
avatar.setBackgroundResource(R.drawable.rectangle)
}
if (!isInEditMode) {
updateView()
}
}
private fun updateView() {
avatars.forEachIndexed { index, avatar ->
avatar.visibility = if (contacts.size > index) View.VISIBLE else View.GONE
avatar.contact = if (contacts.size > index) contacts[index] else null
}
}
}
@@ -0,0 +1,64 @@
/*
* 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 presentation.common.widget
import android.content.Context
import android.util.AttributeSet
import android.widget.ImageView
import android.widget.LinearLayout
import injection.appComponent
import common.util.GlideApp
import common.util.extensions.setVisible
import model.MmsPart
import presentation.common.Navigator
import javax.inject.Inject
class MmsPreviewView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : LinearLayout(context, attrs) {
@Inject lateinit var navigator: Navigator
var parts: List<MmsPart> = ArrayList()
set(value) {
field = value
updateView()
}
init {
appComponent.inject(this)
orientation = LinearLayout.VERTICAL
}
private fun updateView() {
val images = parts.filter { it.image != null }
setVisible(images.isNotEmpty())
if (childCount > 0) {
removeAllViews()
}
images.forEach { image ->
addView(ImageView(context).apply {
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
GlideApp.with(context).load(image.image).fitCenter().into(this)
setOnClickListener { navigator.showImage(image.id) }
})
}
}
}
@@ -0,0 +1,97 @@
/*
* 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 presentation.common.widget
import android.content.Context
import android.content.res.ColorStateList
import android.support.v4.view.ViewPager
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
import android.widget.TextView
import com.moez.QKSMS.R
import com.uber.autodispose.android.scope
import com.uber.autodispose.kotlin.autoDisposable
import injection.appComponent
import common.util.Colors
import common.util.extensions.forEach
import io.reactivex.rxkotlin.Observables
import kotlinx.android.synthetic.main.tab_view.view.*
import javax.inject.Inject
class PagerTitleView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : LinearLayout(context, attrs) {
@Inject lateinit var colors: Colors
var pager: ViewPager? = null
set(value) {
if (field !== value) {
field = value
recreate()
}
}
init {
if (!isInEditMode) appComponent.inject(this)
}
private fun recreate() {
removeAllViews()
pager?.adapter?.count?.forEach { position ->
val view = LayoutInflater.from(context).inflate(R.layout.tab_view, this, false)
view.label.text = pager?.adapter?.getPageTitle(position)
view.setOnClickListener { pager?.currentItem = position }
addView(view)
}
pager?.addOnPageChangeListener(object : ViewPager.SimpleOnPageChangeListener() {
override fun onPageSelected(position: Int) {
childCount.forEach { index ->
getChildAt(index).isSelected = index == position
}
}
})
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
val states = arrayOf(
intArrayOf(android.R.attr.state_selected),
intArrayOf(-android.R.attr.state_selected))
Observables
.combineLatest(colors.theme, colors.textSecondary, { theme, textSecondary ->
ColorStateList(states, intArrayOf(theme, textSecondary))
})
.autoDisposable(scope())
.subscribe { colorStateList ->
childCount.forEach { index ->
(getChildAt(index) as? TextView)?.setTextColor(colorStateList)
}
}
colors.toolbarColor
.autoDisposable(scope())
.subscribe { color -> setBackgroundColor(color) }
}
}
@@ -0,0 +1,110 @@
/*
* 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 presentation.common.widget
import android.content.Context
import android.support.v7.widget.LinearLayoutCompat
import android.util.AttributeSet
import android.view.Gravity
import android.view.View
import android.widget.TextView
import com.moez.QKSMS.R
import com.uber.autodispose.android.scope
import com.uber.autodispose.kotlin.autoDisposable
import injection.appComponent
import common.util.Colors
import common.util.extensions.getColorCompat
import common.util.extensions.setTint
import common.util.extensions.setVisible
import kotlinx.android.synthetic.main.preference_view.view.*
import javax.inject.Inject
class PreferenceView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : LinearLayoutCompat(context, attrs) {
@Inject lateinit var colors: Colors
var title: String? = null
set(value) {
field = value
if (isInEditMode) {
findViewById<TextView>(R.id.titleView).text = value
} else {
titleView.text = value
}
}
var summary: String? = null
set(value) {
field = value
if (isInEditMode) {
findViewById<TextView>(R.id.summaryView).run {
text = value
setVisible(value?.isNotEmpty() == true)
}
} else {
summaryView.text = value
summaryView.setVisible(value?.isNotEmpty() == true)
}
}
init {
if (!isInEditMode) {
appComponent.inject(this)
}
View.inflate(context, R.layout.preference_view, this)
setBackgroundResource(R.drawable.ripple)
orientation = HORIZONTAL
gravity = Gravity.CENTER_VERTICAL
context.obtainStyledAttributes(attrs, R.styleable.PreferenceView)?.run {
title = getString(R.styleable.PreferenceView_title)
summary = getString(R.styleable.PreferenceView_summary)
// If there's a custom view used for the preference's widget, inflate it
getResourceId(R.styleable.PreferenceView_widget, -1).takeIf { it != -1 }?.let { id ->
View.inflate(context, id, widgetFrame)
}
// If an icon is being used, set up the icon view
getResourceId(R.styleable.PreferenceView_icon, -1).takeIf { it != -1 }?.let { id ->
icon.setVisible(true)
icon.setImageResource(id)
}
recycle()
}
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
if (isInEditMode) {
icon.setTint(context.getColorCompat(R.color.textSecondary))
} else {
colors.textSecondary
.autoDisposable(scope())
.subscribe { icon.setTint(it) }
}
}
}
@@ -0,0 +1,84 @@
/*
* 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 presentation.common.widget
import android.content.Context
import android.graphics.Typeface
import android.util.AttributeSet
import android.widget.EditText
import com.moez.QKSMS.R
import com.uber.autodispose.android.scope
import com.uber.autodispose.kotlin.autoDisposable
import injection.appComponent
import common.util.Colors
import common.util.FontProvider
import io.reactivex.Observable
import javax.inject.Inject
/**
* Custom implementation of EditText to allow for dynamic text colors
*
* Beware of updating to extend AppCompatTextView, as this inexplicably breaks the view in
* the contacts chip view
*/
class QkEditText @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : EditText(context, attrs) {
@Inject lateinit var colors: Colors
@Inject lateinit var fontProvider: FontProvider
private var textColorObservable: Observable<Int>? = null
private var textColorHintObservable: Observable<Int>? = null
init {
appComponent.inject(this)
fontProvider.getLato { setTypeface(it, typeface?.style ?: Typeface.NORMAL) }
context.obtainStyledAttributes(attrs, R.styleable.QkEditText)?.run {
textColorObservable = when (getInt(R.styleable.QkEditText_textColor, -1)) {
0 -> colors.textPrimary
1 -> colors.textSecondary
2 -> colors.textTertiary
3 -> colors.textPrimaryOnTheme
4 -> colors.textSecondaryOnTheme
5 -> colors.textTertiaryOnTheme
else -> null
}
textColorHintObservable = when (getInt(R.styleable.QkEditText_textColorHint, -1)) {
0 -> colors.textPrimary
1 -> colors.textSecondary
2 -> colors.textTertiary
else -> null
}
recycle()
}
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
textColorObservable
?.autoDisposable(scope())
?.subscribe { color -> setTextColor(color) }
textColorHintObservable
?.autoDisposable(scope())
?.subscribe { color -> setHintTextColor(color) }
}
}
@@ -0,0 +1,65 @@
/*
* 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 presentation.common.widget
import android.content.Context
import android.content.res.ColorStateList
import android.support.v7.widget.SwitchCompat
import android.util.AttributeSet
import com.uber.autodispose.android.scope
import com.uber.autodispose.kotlin.autoDisposable
import injection.appComponent
import common.util.Colors
import common.util.extensions.withAlpha
import io.reactivex.rxkotlin.Observables
import javax.inject.Inject
class QkSwitch @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : SwitchCompat(context, attrs) {
@Inject lateinit var colors: Colors
init {
if (!isInEditMode) {
appComponent.inject(this)
}
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
if (!isInEditMode) {
val states = arrayOf(
intArrayOf(-android.R.attr.state_enabled),
intArrayOf(android.R.attr.state_checked),
intArrayOf())
Observables.combineLatest(colors.theme, colors.switchThumbEnabled, colors.switchThumbDisabled,
{ color, enabled, disabled -> intArrayOf(disabled, color, enabled) })
.map { values -> ColorStateList(states, values) }
.autoDisposable(scope())
.subscribe { tintList -> thumbTintList = tintList }
Observables.combineLatest(colors.theme, colors.switchTrackEnabled, colors.switchTrackDisabled,
{ color, enabled, disabled -> intArrayOf(disabled, color.withAlpha(0x4D), enabled) })
.map { values -> ColorStateList(states, values) }
.autoDisposable(scope())
.subscribe { tintList -> trackTintList = tintList }
}
}
}
@@ -0,0 +1,113 @@
/*
* 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 presentation.common.widget
import android.content.Context
import android.graphics.Typeface
import android.support.text.emoji.widget.EmojiAppCompatTextView
import android.util.AttributeSet
import com.moez.QKSMS.R
import injection.appComponent
import common.util.Colors
import common.util.FontProvider
import common.util.extensions.getColorCompat
import io.reactivex.Observable
import io.reactivex.disposables.Disposable
import javax.inject.Inject
open class QkTextView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null)
: EmojiAppCompatTextView(context, attrs) {
companion object {
const val COLOR_PRIMARY = 0
const val COLOR_SECONDARY = 1
const val COLOR_TERTIARY = 2
const val COLOR_PRIMARY_ON_THEME = 3
const val COLOR_SECONDARY_ON_THEME = 4
const val COLOR_TERTIARY_ON_THEME = 5
}
@Inject lateinit var colors: Colors
@Inject lateinit var fontProvider: FontProvider
var textColorObservable: Observable<Int>? = null
set(value) {
field = value
updateSubscription()
}
private var textColorDisposable: Disposable? = null
set(value) {
field?.dispose()
field = value
}
init {
if (!isInEditMode) {
appComponent.inject(this)
fontProvider.getLato { setTypeface(it, typeface?.style ?: Typeface.NORMAL) }
}
context.obtainStyledAttributes(attrs, R.styleable.QkTextView)?.run {
val colorAttr = getInt(R.styleable.QkTextView_textColor, -1)
if (isInEditMode) {
setTextColor(context.getColorCompat(when (colorAttr) {
COLOR_PRIMARY -> R.color.textPrimary
COLOR_SECONDARY -> R.color.textSecondary
COLOR_TERTIARY -> R.color.textTertiary
COLOR_PRIMARY_ON_THEME -> R.color.textPrimaryDark
COLOR_SECONDARY_ON_THEME -> R.color.textSecondaryDark
COLOR_TERTIARY_ON_THEME -> R.color.textTertiaryDark
else -> R.color.textPrimary
}))
} else {
textColorObservable = when (colorAttr) {
COLOR_PRIMARY -> colors.textPrimary
COLOR_SECONDARY -> colors.textSecondary
COLOR_TERTIARY -> colors.textTertiary
COLOR_PRIMARY_ON_THEME -> colors.textPrimaryOnTheme
COLOR_SECONDARY_ON_THEME -> colors.textSecondaryOnTheme
COLOR_TERTIARY_ON_THEME -> colors.textTertiaryOnTheme
else -> null
}
}
recycle()
}
}
private fun updateSubscription() {
if (isAttachedToWindow) {
textColorDisposable = textColorObservable?.subscribe { color ->
setTextColor(color)
setLinkTextColor(color)
}
}
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
updateSubscription()
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
textColorDisposable?.dispose()
}
}
@@ -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 presentation.common.widget
import android.content.Context
import android.util.AttributeSet
import android.view.View
import com.moez.QKSMS.R
import com.uber.autodispose.android.scope
import com.uber.autodispose.kotlin.autoDisposable
import injection.appComponent
import common.util.Colors
import common.util.extensions.getColorCompat
import javax.inject.Inject
class Separator @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : View(context, attrs) {
@Inject lateinit var colors: Colors
init {
if (!isInEditMode) {
appComponent.inject(this)
}
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
if (isInEditMode) {
setBackgroundColor(context.getColorCompat(R.color.separatorLight))
} else {
colors.separator
.autoDisposable(scope())
.subscribe { color -> setBackgroundColor(color) }
}
}
}
@@ -0,0 +1,31 @@
/*
* 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 presentation.common.widget
import android.content.Context
import android.util.AttributeSet
import android.widget.ImageView
class SquareImageView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : ImageView(context, attrs) {
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, widthMeasureSpec)
}
}
@@ -0,0 +1,41 @@
/*
* 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 presentation.common.widget
import android.content.Context
import android.util.AttributeSet
class TightTextView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : QkTextView(context, attrs) {
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
layout?.let {
val maxLineWidth = (0 until layout.lineCount)
.asSequence()
.map { layout.getLineMax(it) }
.max() ?: 0f
val width = (Math.ceil(maxLineWidth.toDouble()).toInt() + compoundPaddingLeft + compoundPaddingRight)
val height = measuredHeight
setMeasuredDimension(width, height)
}
}
}