Use observer pattern for label change

This commit is contained in:
Christian Basler 2018-01-13 21:59:20 +01:00
parent 4c89bfe1cf
commit df581f4c51
8 changed files with 130 additions and 113 deletions

View File

@ -17,7 +17,7 @@ android {
buildToolsVersion "26.0.2" buildToolsVersion "26.0.2"
defaultConfig { defaultConfig {
applicationId "ch.dissem.apps." + appName.toLowerCase() applicationId "ch.dissem.apps.${appName.toLowerCase()}"
minSdkVersion 19 minSdkVersion 19
targetSdkVersion 27 targetSdkVersion 27
versionCode 17 versionCode 17

View File

@ -127,8 +127,6 @@ abstract class AbstractItemListFragment<L, T> : ListFragment(), ListHolder<L> {
activatedPosition = position activatedPosition = position
} }
override var currentLabel: L? = null
override fun showPreviousList() = false override fun showPreviousList() = false
/** /**

View File

@ -24,7 +24,5 @@ interface ListHolder<L> {
fun setActivateOnItemClick(activateOnItemClick: Boolean) fun setActivateOnItemClick(activateOnItemClick: Boolean)
var currentLabel: L?
fun showPreviousList(): Boolean fun showPreviousList(): Boolean
} }

View File

@ -30,6 +30,7 @@ import ch.dissem.apps.abit.drawer.ProfileSelectionListener
import ch.dissem.apps.abit.listener.ListSelectionListener import ch.dissem.apps.abit.listener.ListSelectionListener
import ch.dissem.apps.abit.repository.AndroidLabelRepository.Companion.LABEL_ARCHIVE import ch.dissem.apps.abit.repository.AndroidLabelRepository.Companion.LABEL_ARCHIVE
import ch.dissem.apps.abit.service.Singleton import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.service.Singleton.currentLabel
import ch.dissem.apps.abit.synchronization.SyncAdapter import ch.dissem.apps.abit.synchronization.SyncAdapter
import ch.dissem.apps.abit.util.Labels import ch.dissem.apps.abit.util.Labels
import ch.dissem.apps.abit.util.NetworkUtils import ch.dissem.apps.abit.util.NetworkUtils
@ -89,9 +90,6 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
var hasDetailPane: Boolean = false var hasDetailPane: Boolean = false
private set private set
var selectedLabel: Label? = null
private set
private lateinit var bmc: BitmessageContext private lateinit var bmc: BitmessageContext
private lateinit var accountHeader: AccountHeader private lateinit var accountHeader: AccountHeader
@ -280,14 +278,16 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
uiThread { uiThread {
if (intent.hasExtra(EXTRA_SHOW_LABEL)) { if (intent.hasExtra(EXTRA_SHOW_LABEL)) {
selectedLabel = intent.getSerializableExtra(EXTRA_SHOW_LABEL) as Label currentLabel.value = intent.getSerializableExtra(EXTRA_SHOW_LABEL) as Label
} else if (selectedLabel == null) { } else if (currentLabel.value == null) {
selectedLabel = labels[0] currentLabel.value = labels[0]
} }
for (label in labels) { for (label in labels) {
addLabelEntry(label) addLabelEntry(label)
} }
drawer.setSelection(selectedLabel?.id as Long) currentLabel.value?.let {
drawer.setSelection(it.id as Long)
}
updateUnread() updateUnread()
} }
} }
@ -295,16 +295,9 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
override fun onBackPressed() { override fun onBackPressed() {
val listFragment = supportFragmentManager.findFragmentById(R.id.item_list) val listFragment = supportFragmentManager.findFragmentById(R.id.item_list)
if (listFragment is ListHolder<*>) { if (listFragment !is ListHolder<*> || !listFragment.showPreviousList()) {
val listHolder = listFragment as ListHolder<*> super.onBackPressed()
if (listHolder.showPreviousList()) {
drawer.getDrawerItem(listHolder.currentLabel)?.let {
drawer.setSelection(it)
}
return
}
} }
super.onBackPressed()
} }
private inner class DrawerItemClickListener : Drawer.OnDrawerItemClickListener { private inner class DrawerItemClickListener : Drawer.OnDrawerItemClickListener {
@ -312,13 +305,9 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
val itemList = supportFragmentManager.findFragmentById(R.id.item_list) val itemList = supportFragmentManager.findFragmentById(R.id.item_list)
val tag = item.tag val tag = item.tag
if (tag is Label) { if (tag is Label) {
selectedLabel = tag currentLabel.value = tag
if (itemList is MessageListFragment) { if (itemList !is MessageListFragment) {
itemList.updateList(tag) changeList(MessageListFragment())
} else {
val listFragment = MessageListFragment()
changeList(listFragment)
listFragment.updateList(tag)
} }
return false return false
} else if (item is Nameable<*>) { } else if (item is Nameable<*>) {
@ -347,33 +336,23 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
} }
} }
override fun onSaveInstanceState(savedInstanceState: Bundle) {
super.onSaveInstanceState(savedInstanceState)
savedInstanceState.putSerializable("selectedLabel", selectedLabel)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
selectedLabel = savedInstanceState.getSerializable("selectedLabel") as? Label
selectedLabel?.let { selectedLabel ->
drawer.getDrawerItem(selectedLabel)?.let { selectedItem ->
drawer.setSelection(selectedItem)
}
}
super.onRestoreInstanceState(savedInstanceState)
}
override fun onResume() { override fun onResume() {
updateUnread() updateUnread()
if (Preferences.isFullNodeActive(this) && Preferences.isConnectionAllowed(this@MainActivity)) { if (Preferences.isFullNodeActive(this) && Preferences.isConnectionAllowed(this@MainActivity)) {
NetworkUtils.enableNode(this, false) NetworkUtils.enableNode(this, false)
} }
Singleton.getMessageListener(this).resetNotification() Singleton.getMessageListener(this).resetNotification()
currentLabel.addObserver(this) { label ->
if (label != null) {
drawer.setSelection(label.id as Long)
}
}
active = true active = true
super.onResume() super.onResume()
} }
override fun onPause() { override fun onPause() {
currentLabel.removeObserver(this)
super.onPause() super.onPause()
active = false active = false
} }
@ -471,9 +450,7 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
// for the selected item ID. // for the selected item ID.
val detailIntent = when (item) { val detailIntent = when (item) {
is Plaintext -> { is Plaintext -> {
Intent(this, MessageDetailActivity::class.java).apply { Intent(this, MessageDetailActivity::class.java)
putExtra(EXTRA_SHOW_LABEL, selectedLabel)
}
} }
is BitmessageAddress -> Intent(this, AddressDetailActivity::class.java) is BitmessageAddress -> Intent(this, AddressDetailActivity::class.java)
else -> throw IllegalArgumentException("Plaintext or BitmessageAddress expected, but was ${item::class.simpleName}") else -> throw IllegalArgumentException("Plaintext or BitmessageAddress expected, but was ${item::class.simpleName}")
@ -521,7 +498,7 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
* Runs the given code in the main activity context, if it currently exists. Otherwise, * Runs the given code in the main activity context, if it currently exists. Otherwise,
* it's ignored. * it's ignored.
*/ */
fun apply(run: MainActivity.() -> Unit){ fun apply(run: MainActivity.() -> Unit) {
instance?.get()?.let { run.invoke(it) } instance?.get()?.let { run.invoke(it) }
} }
} }

View File

@ -5,8 +5,6 @@ import android.os.Bundle
import android.support.v4.app.NavUtils import android.support.v4.app.NavUtils
import android.view.MenuItem import android.view.MenuItem
import ch.dissem.bitmessage.entity.valueobject.Label
/** /**
* An activity representing a single Message detail screen. This * An activity representing a single Message detail screen. This
@ -18,7 +16,6 @@ import ch.dissem.bitmessage.entity.valueobject.Label
* more than a [MessageDetailFragment]. * more than a [MessageDetailFragment].
*/ */
class MessageDetailActivity : DetailActivity() { class MessageDetailActivity : DetailActivity() {
private var label: Label? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -33,7 +30,6 @@ class MessageDetailActivity : DetailActivity() {
// http://developer.android.com/guide/components/fragments.html // http://developer.android.com/guide/components/fragments.html
// //
if (savedInstanceState == null) { if (savedInstanceState == null) {
label = intent.getSerializableExtra(MainActivity.EXTRA_SHOW_LABEL) as Label?
// Create the detail fragment and add it to the activity // Create the detail fragment and add it to the activity
// using a fragment transaction. // using a fragment transaction.
val arguments = Bundle() val arguments = Bundle()
@ -49,9 +45,7 @@ class MessageDetailActivity : DetailActivity() {
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
android.R.id.home -> { android.R.id.home -> {
val parentIntent = Intent(this, MainActivity::class.java) NavUtils.navigateUpTo(this, Intent(this, MainActivity::class.java))
parentIntent.putExtra(MainActivity.EXTRA_SHOW_LABEL, label)
NavUtils.navigateUpTo(this, parentIntent)
true true
} }
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)

View File

@ -32,6 +32,7 @@ import ch.dissem.apps.abit.adapter.SwipeableMessageAdapter
import ch.dissem.apps.abit.listener.ListSelectionListener import ch.dissem.apps.abit.listener.ListSelectionListener
import ch.dissem.apps.abit.repository.AndroidMessageRepository import ch.dissem.apps.abit.repository.AndroidMessageRepository
import ch.dissem.apps.abit.service.Singleton import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.service.Singleton.currentLabel
import ch.dissem.apps.abit.util.FabUtils import ch.dissem.apps.abit.util.FabUtils
import ch.dissem.bitmessage.entity.Plaintext import ch.dissem.bitmessage.entity.Plaintext
import ch.dissem.bitmessage.entity.valueobject.Label import ch.dissem.bitmessage.entity.valueobject.Label
@ -87,8 +88,6 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
} }
} }
override var currentLabel: Label? = null
private var emptyTrashMenuItem: MenuItem? = null private var emptyTrashMenuItem: MenuItem? = null
private lateinit var messageRepo: AndroidMessageRepository private lateinit var messageRepo: AndroidMessageRepository
private var activateOnItemClick: Boolean = false private var activateOnItemClick: Boolean = false
@ -99,7 +98,7 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
isLoading = true isLoading = true
swipeableMessageAdapter?.let { messageAdapter -> swipeableMessageAdapter?.let { messageAdapter ->
doAsync { doAsync {
val messages = messageRepo.findMessages(currentLabel, messageAdapter.itemCount, PAGE_SIZE) val messages = messageRepo.findMessages(currentLabel.value, messageAdapter.itemCount, PAGE_SIZE)
onUiThread { onUiThread {
messageAdapter.addAll(messages) messageAdapter.addAll(messages)
isLoading = false isLoading = false
@ -121,21 +120,13 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
initFab(activity) initFab(activity)
messageRepo = Singleton.getMessageRepository(activity) messageRepo = Singleton.getMessageRepository(activity)
if (backStack.isEmpty() && currentLabel == null) { currentLabel.addObserver(this) { new -> doUpdateList(new) }
doUpdateList(activity.selectedLabel) doUpdateList(currentLabel.value)
}
} }
override fun updateList(label: Label) { override fun onPause() {
if (currentLabel != null && currentLabel != label && (backStack.isEmpty() || currentLabel != backStack.peek())) { currentLabel.removeObserver(this)
backStack.push(currentLabel) super.onPause()
}
if (!isResumed) {
currentLabel = label
return
}
doUpdateList(label)
} }
private fun doUpdateList(label: Label?) { private fun doUpdateList(label: Label?) {
@ -146,7 +137,6 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
swipeableMessageAdapter?.notifyDataSetChanged() swipeableMessageAdapter?.notifyDataSetChanged()
return return
} }
currentLabel = label
emptyTrashMenuItem?.isVisible = label.type == Label.Type.TRASH emptyTrashMenuItem?.isVisible = label.type == Label.Type.TRASH
mainActivity?.apply { mainActivity?.apply {
if ("archive" == label.toString()) { if ("archive" == label.toString()) {
@ -238,36 +228,7 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
recyclerViewSwipeManager = swipeManager recyclerViewSwipeManager = swipeManager
this.swipeableMessageAdapter = adapter this.swipeableMessageAdapter = adapter
Singleton.labeler.listener = { message, added, removed -> Singleton.updateMessageListAdapterInListener(adapter)
swipeableMessageAdapter?.let { swipeableMessageAdapter ->
when {
currentLabel?.type == Label.Type.TRASH && added.all { it.type == Label.Type.TRASH } && removed.any { it.type == Label.Type.TRASH } -> {
// work-around for messages that are deleted from trash
swipeableMessageAdapter.remove(message)
}
currentLabel?.type == Label.Type.UNREAD && added.all { it.type == Label.Type.TRASH } -> {
// work-around for messages that are deleted from unread, which already have the unread label removed
swipeableMessageAdapter.remove(message)
}
added.contains(currentLabel) -> {
// in most cases, top should be the correct position, but time will show if
// the message should be properly sorted in
swipeableMessageAdapter.addFirst(message)
}
removed.contains(currentLabel) -> {
swipeableMessageAdapter.remove(message)
}
removed.any { it.type == Label.Type.UNREAD } || added.any { it.type == Label.Type.UNREAD } -> {
swipeableMessageAdapter.update(message)
}
}
}
if (removed.any { it.type == Label.Type.UNREAD } || added.any { it.type == Label.Type.UNREAD }) {
MainActivity.apply {
updateUnread()
}
}
}
} }
private fun initFab(context: MainActivity) { private fun initFab(context: MainActivity) {
@ -326,24 +287,27 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
currentLabel?.let { currentLabel -> when (item.itemId) {
when (item.itemId) { R.id.empty_trash -> {
R.id.empty_trash -> { currentLabel.value?.let { label ->
if (currentLabel.type != Label.Type.TRASH) return true if (label.type != Label.Type.TRASH) return true
doAsync { doAsync {
for (message in messageRepo.findMessages(currentLabel)) { for (message in messageRepo.findMessages(label)) {
messageRepo.remove(message) messageRepo.remove(message)
} }
uiThread { updateList(currentLabel) } uiThread { doUpdateList(label) }
} }
return true
} }
else -> return false return true
} }
else -> return false
} }
return false }
override fun updateList(label: Label) {
currentLabel.value = label
} }
override fun setActivateOnItemClick(activateOnItemClick: Boolean) { override fun setActivateOnItemClick(activateOnItemClick: Boolean) {
@ -354,7 +318,7 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
override fun showPreviousList() = if (backStack.isEmpty()) { override fun showPreviousList() = if (backStack.isEmpty()) {
false false
} else { } else {
doUpdateList(backStack.pop()) currentLabel.value = backStack.pop()
true true
} }
} }

View File

@ -21,14 +21,17 @@ import android.widget.Toast
import ch.dissem.apps.abit.MainActivity import ch.dissem.apps.abit.MainActivity
import ch.dissem.apps.abit.R import ch.dissem.apps.abit.R
import ch.dissem.apps.abit.adapter.AndroidCryptography import ch.dissem.apps.abit.adapter.AndroidCryptography
import ch.dissem.apps.abit.adapter.SwipeableMessageAdapter
import ch.dissem.apps.abit.adapter.SwitchingProofOfWorkEngine import ch.dissem.apps.abit.adapter.SwitchingProofOfWorkEngine
import ch.dissem.apps.abit.listener.MessageListener import ch.dissem.apps.abit.listener.MessageListener
import ch.dissem.apps.abit.pow.ServerPowEngine import ch.dissem.apps.abit.pow.ServerPowEngine
import ch.dissem.apps.abit.repository.* import ch.dissem.apps.abit.repository.*
import ch.dissem.apps.abit.util.Constants import ch.dissem.apps.abit.util.Constants
import ch.dissem.apps.abit.util.Observable
import ch.dissem.bitmessage.BitmessageContext import ch.dissem.bitmessage.BitmessageContext
import ch.dissem.bitmessage.entity.BitmessageAddress import ch.dissem.bitmessage.entity.BitmessageAddress
import ch.dissem.bitmessage.entity.payload.Pubkey import ch.dissem.bitmessage.entity.payload.Pubkey
import ch.dissem.bitmessage.entity.valueobject.Label
import ch.dissem.bitmessage.networking.nio.NioNetworkHandler import ch.dissem.bitmessage.networking.nio.NioNetworkHandler
import ch.dissem.bitmessage.ports.DefaultLabeler import ch.dissem.bitmessage.ports.DefaultLabeler
import ch.dissem.bitmessage.utils.ConversationService import ch.dissem.bitmessage.utils.ConversationService
@ -36,12 +39,54 @@ import ch.dissem.bitmessage.utils.TTL
import ch.dissem.bitmessage.utils.UnixTime.DAY import ch.dissem.bitmessage.utils.UnixTime.DAY
import org.jetbrains.anko.doAsync import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread import org.jetbrains.anko.uiThread
import java.lang.ref.WeakReference
/** /**
* Provides singleton objects across the application. * Provides singleton objects across the application.
*/ */
object Singleton { object Singleton {
val labeler = DefaultLabeler() var currentLabel = Observable<Label?>(null).apply {
addObserver(this) { _ -> swipeableMessageAdapter = null }
}
private var swipeableMessageAdapter: WeakReference<SwipeableMessageAdapter>? = null
val labeler = DefaultLabeler().apply {
listener = { message, added, removed ->
swipeableMessageAdapter?.get()?.let { swipeableMessageAdapter ->
currentLabel.value?.let { label ->
when {
label.type == Label.Type.TRASH
&& added.all { it.type == Label.Type.TRASH }
&& removed.any { it.type == Label.Type.TRASH } -> {
// work-around for messages that are deleted from trash
swipeableMessageAdapter.remove(message)
}
label.type == Label.Type.UNREAD
&& added.all { it.type == Label.Type.TRASH } -> {
// work-around for messages that are deleted from unread, which already have the unread label removed
swipeableMessageAdapter.remove(message)
}
added.contains(label) -> {
// in most cases, top should be the correct position, but time will show if
// the message should be properly sorted in
swipeableMessageAdapter.addFirst(message)
}
removed.contains(label) -> {
swipeableMessageAdapter.remove(message)
}
removed.any { it.type == Label.Type.UNREAD } || added.any { it.type == Label.Type.UNREAD } -> {
swipeableMessageAdapter.update(message)
}
}
}
}
if (removed.any { it.type == Label.Type.UNREAD } || added.any { it.type == Label.Type.UNREAD }) {
MainActivity.apply {
updateUnread()
}
}
}
}
var bitmessageContext: BitmessageContext? = null var bitmessageContext: BitmessageContext? = null
private set private set
private var conversationService: ConversationService? = null private var conversationService: ConversationService? = null
@ -75,6 +120,10 @@ object Singleton {
} }
} }
fun updateMessageListAdapterInListener(adapter: SwipeableMessageAdapter) {
swipeableMessageAdapter = WeakReference(adapter)
}
fun getMessageListener(ctx: Context) = init({ messageListener }, { messageListener = it }) { MessageListener(ctx) } fun getMessageListener(ctx: Context) = init({ messageListener }, { messageListener = it }) { MessageListener(ctx) }
fun getLabelRepository(ctx: Context) = getBitmessageContext(ctx).labels as AndroidLabelRepository fun getLabelRepository(ctx: Context) = getBitmessageContext(ctx).labels as AndroidLabelRepository

View File

@ -0,0 +1,37 @@
package ch.dissem.apps.abit.util
import kotlin.properties.Delegates
/**
* A simple observable implementation that should be mostly
*/
class Observable<T>(value: T) {
private val observers = mutableMapOf<Any, (T) -> Unit>()
var value: T by Delegates.observable(value, { _, old, new ->
if (old != new) {
observers.values.forEach { it.invoke(new) }
}
})
/**
* The key will make sure the observer can easily be removed. Usually the key should be either
* the object that created the observer, or the observer itself, if it's easily available.
*
* Note that a map is used for observers, so if you define more than one observer with the same
* key, all previous ones will be removed. Also, the observers will be notified in no specific
* order.
*
* To prevent memory leaks, the observer must be removed if it isn't used anymore.
*/
fun addObserver(key: Any, observer: (T) -> Unit) {
observers.put(key, observer)
}
/**
* Remove the observer that was registered with the given key.
*/
fun removeObserver(key: Any) {
observers.remove(key)
}
}