diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cd8b5ce71eec405ff70ad76bd9eecb84353cd31e..a38f2ac74d457d72e09bc3254aefb31b74e090c5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -128,8 +128,8 @@ dependencies { withVersion("1.1.1") { implementation("android.arch.lifecycle", "extensions", version) implementation("android.arch.lifecycle", "reactivestreams", version) - kapt("android.arch.lifecycle", "compiler", version) testImplementation("android.arch.core", "core-testing", version) + implementation(project(":lifecycle-ktx")) } // App Arch Persistence diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ChatActivity.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ChatActivity.kt index 5f5fb4c831e45d0e62a7c3a6976ad1ab3d38a246..80b350d11b92c2c84c2dfb7aa80a328a99801365 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ChatActivity.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/ChatActivity.kt @@ -19,7 +19,6 @@ package de.kuschku.quasseldroid.ui.chat -import android.annotation.TargetApi import android.app.Activity import android.arch.lifecycle.Observer import android.content.Context @@ -27,7 +26,6 @@ import android.content.Intent import android.content.SharedPreferences import android.os.Build import android.os.Bundle -import android.os.PersistableBundle import android.support.design.widget.BottomSheetBehavior import android.support.v4.widget.DrawerLayout import android.support.v7.app.ActionBarDrawerToggle @@ -125,6 +123,8 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc private var connectedAccount = -1L + private var restoredDrawerState = false + override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) if (intent != null) { @@ -157,8 +157,9 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc setSupportActionBar(toolbar) - viewModel.buffer.toLiveData().observe(this, Observer { - if (it != null && drawerLayout.isDrawerOpen(Gravity.START)) { + viewModel.bufferOpened.toLiveData().observe(this, Observer { + actionMode?.finish() + if (drawerLayout.isDrawerOpen(Gravity.START)) { drawerLayout.closeDrawer(Gravity.START, true) } }) @@ -459,7 +460,8 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc .observe(this, Observer { if (connectedAccount != accountId) { if (resources.getBoolean(R.bool.buffer_drawer_exists) && - viewModel.buffer.value == Int.MAX_VALUE) { + viewModel.buffer.value == Int.MAX_VALUE && + !restoredDrawerState) { drawerLayout.openDrawer(Gravity.START) } connectedAccount = accountId @@ -551,6 +553,7 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc var bufferData: BufferData? = null var actionMode: ActionMode? = null + private var statusBarColor: Int? = null override fun onActionModeStarted(mode: ActionMode?) { when (mode?.tag) { @@ -558,12 +561,24 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc "MESSAGES" -> mode.menu?.retint(toolbar.context) } actionMode = mode + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + statusBarColor = window.statusBarColor + window.statusBarColor = theme.styledAttributes(R.attr.colorPrimaryDark) { + getColor(0, 0) + } + } super.onActionModeStarted(mode) } override fun onActionModeFinished(mode: ActionMode?) { actionMode = null super.onActionModeFinished(mode) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + statusBarColor?.let { + window.statusBarColor = it + statusBarColor = null + } + } } override fun onStart() { @@ -581,18 +596,8 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc outState?.putInt("OPEN_BUFFER", viewModel.buffer.value ?: -1) outState?.putInt("OPEN_BUFFERVIEWCONFIG", viewModel.bufferViewConfigId.value ?: -1) outState?.putLong("CONNECTED_ACCOUNT", connectedAccount) - } - - override fun onSaveInstanceState(outState: Bundle?, outPersistentState: PersistableBundle?) { - super.onSaveInstanceState(outState, outPersistentState) - outState?.putInt("OPEN_BUFFER", viewModel.buffer.value ?: -1) - outState?.putInt("OPEN_BUFFERVIEWCONFIG", viewModel.bufferViewConfigId.value ?: -1) - outState?.putLong("CONNECTED_ACCOUNT", connectedAccount) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - outPersistentState?.putInt("OPEN_BUFFER", viewModel.buffer.value ?: -1) - outPersistentState?.putInt("OPEN_BUFFERVIEWCONFIG", viewModel.bufferViewConfigId.value ?: -1) - outPersistentState?.putLong("CONNECTED_ACCOUNT", connectedAccount) - } + outState?.putBoolean("OPEN_DRAWER_START", drawerLayout.isDrawerOpen(Gravity.START)) + outState?.putBoolean("OPEN_DRAWER_END", drawerLayout.isDrawerOpen(Gravity.END)) } override fun onRestoreInstanceState(savedInstanceState: Bundle?) { @@ -601,27 +606,16 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc viewModel.bufferViewConfigId.onNext(savedInstanceState?.getInt("OPEN_BUFFERVIEWCONFIG", -1) ?: -1) connectedAccount = savedInstanceState?.getLong("CONNECTED_ACCOUNT", -1L) ?: -1L - } - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - override fun onRestoreInstanceState(savedInstanceState: Bundle?, - persistentState: PersistableBundle?) { - super.onRestoreInstanceState(savedInstanceState, persistentState) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - val fallbackBuffer = persistentState?.getInt("OPEN_BUFFER", -1) ?: -1 - viewModel.buffer.onNext( - savedInstanceState?.getInt("OPEN_BUFFER", fallbackBuffer) - ?: fallbackBuffer - ) - val fallbackBufferViewConfigId = persistentState?.getInt("OPEN_BUFFERVIEWCONFIG", -1) ?: -1 - viewModel.bufferViewConfigId.onNext( - savedInstanceState?.getInt("OPEN_BUFFERVIEWCONFIG", fallbackBufferViewConfigId) - ?: fallbackBufferViewConfigId - ) - val fallbackConnectedAccount = persistentState?.getLong("CONNECTED_ACCOUNT", -1L) ?: -1L - connectedAccount = savedInstanceState?.getLong( - "CONNECTED_ACCOUNT", fallbackConnectedAccount - ) ?: fallbackConnectedAccount + if (savedInstanceState?.getBoolean("OPEN_DRAWER_START") == true) { + drawerLayout.openDrawer(Gravity.START) + } + if (savedInstanceState?.getBoolean("OPEN_DRAWER_END") == true) { + drawerLayout.openDrawer(Gravity.END) + } + if (savedInstanceState?.getBoolean("OPEN_DRAWER_START") != null || + savedInstanceState?.getBoolean("OPEN_DRAWER_END") != null) { + restoredDrawerState = true } } diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferListAdapter.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferListAdapter.kt index 8e20f0c83f4613aec818b6997f417387d7d93276..be60564e26134a37ed8ea98d1d6849ecdb75e199 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferListAdapter.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferListAdapter.kt @@ -20,7 +20,6 @@ package de.kuschku.quasseldroid.ui.chat.buffers import android.graphics.drawable.Drawable -import android.support.v7.recyclerview.extensions.ListAdapter import android.support.v7.util.DiffUtil import android.support.v7.widget.RecyclerView import android.view.LayoutInflater @@ -40,6 +39,7 @@ import de.kuschku.quasseldroid.util.helper.getVectorDrawableCompat import de.kuschku.quasseldroid.util.helper.styledAttributes import de.kuschku.quasseldroid.util.helper.tint import de.kuschku.quasseldroid.util.helper.visibleIf +import de.kuschku.quasseldroid.util.lists.ListAdapter import de.kuschku.quasseldroid.viewmodel.data.BufferListItem import de.kuschku.quasseldroid.viewmodel.data.BufferProps import de.kuschku.quasseldroid.viewmodel.data.BufferState @@ -60,6 +60,7 @@ class BufferListAdapter( ) { private var clickListener: ((BufferId) -> Unit)? = null private var longClickListener: ((BufferId) -> Unit)? = null + private var updateFinishedListener: ((List<BufferListItem>) -> Unit)? = null fun setOnClickListener(listener: ((BufferId) -> Unit)?) { this.clickListener = listener } @@ -68,6 +69,14 @@ class BufferListAdapter( this.longClickListener = listener } + fun setOnUpdateFinishedListener(listener: ((List<BufferListItem>) -> Unit)?) { + this.updateFinishedListener = listener + } + + override fun onUpdateFinished(list: List<BufferListItem>) { + this.updateFinishedListener?.invoke(list) + } + fun expandListener(networkId: NetworkId) { if (collapsedNetworks.value.orEmpty().contains(networkId)) collapsedNetworks.onNext(collapsedNetworks.value.orEmpty() - networkId) diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferViewConfigAdapter.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferViewConfigAdapter.kt index c21c3b88408a4623d0fb86b6399e7aa48bfaae6b..c829eb24432218006632a379e31464a9ee639249 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferViewConfigAdapter.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferViewConfigAdapter.kt @@ -37,9 +37,17 @@ class BufferViewConfigAdapter : ThemedSpinnerAdapter { var data = emptyList<BufferViewConfig>() + + private var updateFinishedListener: ((List<BufferViewConfig>) -> Unit)? = null + + fun setOnUpdateFinishedListener(listener: ((List<BufferViewConfig>) -> Unit)?) { + this.updateFinishedListener = listener + } + fun submitList(list: List<BufferViewConfig>) { data = list notifyDataSetChanged() + updateFinishedListener?.invoke(list) } fun indexOf(id: Int) = data.indexOfFirst { it.bufferViewId() == id } diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferViewConfigFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferViewConfigFragment.kt index c42ac4d621207d7e0ff39cc5bab66cede14955cf..814b968babe84c89511c65389c1ecd30d20dbdaf 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferViewConfigFragment.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferViewConfigFragment.kt @@ -21,6 +21,7 @@ package de.kuschku.quasseldroid.ui.chat.buffers import android.arch.lifecycle.Observer import android.os.Bundle +import android.os.Parcelable import android.support.v7.widget.* import android.view.* import android.widget.AdapterView @@ -207,21 +208,28 @@ class BufferViewConfigFragment : ServiceBoundFragment() { val view = inflater.inflate(R.layout.fragment_chat_list, container, false) ButterKnife.bind(this, view) - var hasSetBufferViewConfigId = false val adapter = BufferViewConfigAdapter() viewModel.bufferViewConfigs.switchMap { combineLatest(it.map(BufferViewConfig::liveUpdates)) }.toLiveData().observe(this, Observer { if (it != null) { adapter.submitList(it) - if (!hasSetBufferViewConfigId) { - chatListSpinner.setSelection(adapter.indexOf(viewModel.bufferViewConfigId.value)) - hasSetBufferViewConfigId = true - } } }) - chatListSpinner.adapter = adapter + var hasSetBufferViewConfigId = false + var hasRestoredSpinnerState = false + adapter.setOnUpdateFinishedListener { + if (!hasRestoredSpinnerState) { + savedInstanceState?.getParcelable<Parcelable>(KEY_STATE_SPINNER) + ?.let(chatListSpinner::onRestoreInstanceState) + hasRestoredSpinnerState = true + } + if (!hasSetBufferViewConfigId) { + chatListSpinner.setSelection(adapter.indexOf(viewModel.bufferViewConfigId.value)) + hasSetBufferViewConfigId = true + } + } chatListSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { override fun onNothingSelected(adapter: AdapterView<*>?) { if (hasSetBufferViewConfigId) @@ -234,6 +242,7 @@ class BufferViewConfigFragment : ServiceBoundFragment() { viewModel.bufferViewConfigId.onNext(id.toInt()) } } + chatListSpinner.adapter = adapter listAdapter = BufferListAdapter( viewModel.selectedBufferId, @@ -291,6 +300,15 @@ class BufferViewConfigFragment : ServiceBoundFragment() { }) listAdapter.setOnClickListener(this@BufferViewConfigFragment::clickListener) listAdapter.setOnLongClickListener(this@BufferViewConfigFragment::longClickListener) + + var hasRestoredChatListState = false + listAdapter.setOnUpdateFinishedListener { + if (!hasRestoredChatListState) { + savedInstanceState?.getParcelable<Parcelable>(KEY_STATE_LIST) + ?.let(chatList.layoutManager::onRestoreInstanceState) + hasRestoredChatListState = true + } + } chatList.adapter = listAdapter viewModel.selectedBuffer.toLiveData().observe(this, Observer { buffer -> @@ -402,6 +420,7 @@ class BufferViewConfigFragment : ServiceBoundFragment() { longClickListener(it) } else { viewModel.buffer.onNext(it) + viewModel.bufferOpened.onNext(Unit) } } @@ -415,7 +434,7 @@ class BufferViewConfigFragment : ServiceBoundFragment() { } companion object { - private const val KEY_STATE_LIST = "KEY_STATE_LIST" - private const val KEY_STATE_SPINNER = "KEY_STATE_SPINNER" + private const val KEY_STATE_LIST = "STATE_LIST" + private const val KEY_STATE_SPINNER = "STATE_SPINNER" } } diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/lists/AsyncDifferConfig.kt b/app/src/main/java/de/kuschku/quasseldroid/util/lists/AsyncDifferConfig.kt new file mode 100644 index 0000000000000000000000000000000000000000..8861d3bb16b8d0fbcc34141ee8cc4bf3369c0fda --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid/util/lists/AsyncDifferConfig.kt @@ -0,0 +1,127 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.kuschku.quasseldroid.util.lists + +import android.os.Handler +import android.os.Looper +import android.support.annotation.RestrictTo +import android.support.v7.util.DiffUtil + +import java.util.concurrent.Executor +import java.util.concurrent.Executors + +/** + * Configuration object for [ListAdapter], [AsyncListDiffer], and similar + * background-thread list diffing adapter logic. + * + * + * At minimum, defines item diffing behavior with a [DiffUtil.ItemCallback], used to compute + * item differences to pass to a RecyclerView adapter. + * + * @param <T> Type of items in the lists, and being compared. +</T> */ +class AsyncDifferConfig<T> private constructor( + @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + /** @hide + */ + val mainThreadExecutor: Executor, + val backgroundThreadExecutor: Executor, + val diffCallback: DiffUtil.ItemCallback<T>) { + + /** + * Builder class for [AsyncDifferConfig]. + * + * @param <T> + </T> */ + class Builder<T>(private val mDiffCallback: DiffUtil.ItemCallback<T>) { + private var mMainThreadExecutor: Executor? = null + private var mBackgroundThreadExecutor: Executor? = null + + /** + * If provided, defines the main thread executor used to dispatch adapter update + * notifications on the main thread. + * + * + * If not provided, it will default to the main thread. + * + * @param executor The executor which can run tasks in the UI thread. + * @return this + * + * @hide + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + fun setMainThreadExecutor(executor: Executor): Builder<T> { + mMainThreadExecutor = executor + return this + } + + /** + * If provided, defines the background executor used to calculate the diff between an old + * and a new list. + * + * + * If not provided, defaults to two thread pool executor, shared by all ListAdapterConfigs. + * + * @param executor The background executor to run list diffing. + * @return this + */ + fun setBackgroundThreadExecutor(executor: Executor): Builder<T> { + mBackgroundThreadExecutor = executor + return this + } + + private class MainThreadExecutor : Executor { + internal val mHandler = Handler(Looper.getMainLooper()) + override fun execute(command: Runnable) { + mHandler.post(command) + } + } + + /** + * Creates a [AsyncListDiffer] with the given parameters. + * + * @return A new AsyncDifferConfig. + */ + fun build(): AsyncDifferConfig<T> { + if (mMainThreadExecutor == null) { + mMainThreadExecutor = sMainThreadExecutor + } + if (mBackgroundThreadExecutor == null) { + synchronized(sExecutorLock) { + if (sDiffExecutor == null) { + sDiffExecutor = Executors.newFixedThreadPool(2) + } + } + mBackgroundThreadExecutor = sDiffExecutor + } + return AsyncDifferConfig( + mMainThreadExecutor!!, + mBackgroundThreadExecutor!!, + mDiffCallback) + } + + companion object { + + // TODO: remove the below once supportlib has its own appropriate executors + private val sExecutorLock = Any() + private var sDiffExecutor: Executor? = null + + // TODO: use MainThreadExecutor from supportlib once one exists + private val sMainThreadExecutor = MainThreadExecutor() + } + } +} diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/lists/AsyncListDiffer.kt b/app/src/main/java/de/kuschku/quasseldroid/util/lists/AsyncListDiffer.kt new file mode 100644 index 0000000000000000000000000000000000000000..65ef18ec1c057f27a32da767f4411a0b4b16ddc6 --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid/util/lists/AsyncListDiffer.kt @@ -0,0 +1,266 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.kuschku.quasseldroid.util.lists + +import android.support.v7.util.AdapterListUpdateCallback +import android.support.v7.util.DiffUtil +import android.support.v7.util.ListUpdateCallback +import android.support.v7.widget.RecyclerView +import java.util.* + +/** + * Helper for computing the difference between two lists via [DiffUtil] on a background + * thread. + * + * + * It can be connected to a + * [RecyclerView.Adapter][android.support.v7.widget.RecyclerView.Adapter], and will signal the + * adapter of changes between sumbitted lists. + * + * + * For simplicity, the [ListAdapter] wrapper class can often be used instead of the + * AsyncListDiffer directly. This AsyncListDiffer can be used for complex cases, where overriding an + * adapter base class to support asynchronous List diffing isn't convenient. + * + * + * The AsyncListDiffer can consume the values from a LiveData of `List` and present the + * data simply for an adapter. It computes differences in list contents via [DiffUtil] on a + * background thread as new `List`s are received. + * + * + * Use [.getCurrentList] to access the current List, and present its data objects. Diff + * results will be dispatched to the ListUpdateCallback immediately before the current list is + * updated. If you're dispatching list updates directly to an Adapter, this means the Adapter can + * safely access list items and total size via [.getCurrentList]. + * + * + * A complete usage pattern with Room would look like this: + * <pre> + * @Dao + * interface UserDao { + * @Query("SELECT * FROM user ORDER BY lastName ASC") + * public abstract LiveData<List<User>> usersByLastName(); + * } + * + * class MyViewModel extends ViewModel { + * public final LiveData<List<User>> usersList; + * public MyViewModel(UserDao userDao) { + * usersList = userDao.usersByLastName(); + * } + * } + * + * class MyActivity extends AppCompatActivity { + * @Override + * public void onCreate(Bundle savedState) { + * super.onCreate(savedState); + * MyViewModel viewModel = ViewModelProviders.of(this).get(MyViewModel.class); + * RecyclerView recyclerView = findViewById(R.id.user_list); + * UserAdapter adapter = new UserAdapter(); + * viewModel.usersList.observe(this, list -> adapter.submitList(list)); + * recyclerView.setAdapter(adapter); + * } + * } + * + * class UserAdapter extends RecyclerView.Adapter<UserViewHolder> { + * private final AsyncListDiffer<User> mDiffer = new AsyncListDiffer(this, DIFF_CALLBACK); + * @Override + * public int getItemCount() { + * return mDiffer.getCurrentList().size(); + * } + * public void submitList(List<User> list) { + * mDiffer.submitList(list); + * } + * @Override + * public void onBindViewHolder(UserViewHolder holder, int position) { + * User user = mDiffer.getCurrentList().get(position); + * holder.bindTo(user); + * } + * public static final DiffUtil.ItemCallback<User> DIFF_CALLBACK + * = new DiffUtil.ItemCallback<User>() { + * @Override + * public boolean areItemsTheSame( + * @NonNull User oldUser, @NonNull User newUser) { + * // User properties may have changed if reloaded from the DB, but ID is fixed + * return oldUser.getId() == newUser.getId(); + * } + * @Override + * public boolean areContentsTheSame( + * @NonNull User oldUser, @NonNull User newUser) { + * // NOTE: if you use equals, your object must properly override Object#equals() + * // Incorrectly returning false here will result in too many animations. + * return oldUser.equals(newUser); + * } + * } + * }</pre> + * + * @param <T> Type of the lists this AsyncListDiffer will receive. + * + * @see DiffUtil + * + * @see AdapterListUpdateCallback +</T> */ +class AsyncListDiffer<T> { + private val mUpdateCallback: ListUpdateCallback + private val mUpdateFinishedCallback: ((List<T>) -> Unit)? + private val mConfig: AsyncDifferConfig<T> + + private var mList: List<T>? = null + + /** + * Non-null, unmodifiable version of mList. + * + * + * Collections.emptyList when mList is null, wrapped by Collections.unmodifiableList otherwise + */ + /** + * Get the current List - any diffing to present this list has already been computed and + * dispatched via the ListUpdateCallback. + * + * + * If a `null` List, or no List has been submitted, an empty list will be returned. + * + * + * The returned list may not be mutated - mutations to content must be done through + * [.submitList]. + * + * @return current List. + */ + var currentList = emptyList<T>() + private set + + // Max generation of currently scheduled runnable + private var mMaxScheduledGeneration: Int = 0 + + /** + * Convenience for + * `AsyncListDiffer(new AdapterListUpdateCallback(adapter), + * new AsyncDifferConfig.Builder().setDiffCallback(diffCallback).build());` + * + * @param adapter Adapter to dispatch position updates to. + * @param diffCallback ItemCallback that compares items to dispatch appropriate animations when + * + * @see DiffUtil.DiffResult.dispatchUpdatesTo + */ + constructor(adapter: RecyclerView.Adapter<*>, + updateFinishedCallback: ((List<T>) -> Unit)? = null, + diffCallback: DiffUtil.ItemCallback<T>) { + mUpdateCallback = AdapterListUpdateCallback(adapter) + mUpdateFinishedCallback = updateFinishedCallback + mConfig = AsyncDifferConfig.Builder(diffCallback).build() + } + + /** + * Create a AsyncListDiffer with the provided config, and ListUpdateCallback to dispatch + * updates to. + * + * @param listUpdateCallback Callback to dispatch updates to. + * @param config Config to define background work Executor, and DiffUtil.ItemCallback for + * computing List diffs. + * + * @see DiffUtil.DiffResult.dispatchUpdatesTo + */ + constructor(listUpdateCallback: ListUpdateCallback, + updateFinishedCallback: ((List<T>) -> Unit)? = null, + config: AsyncDifferConfig<T>) { + mUpdateCallback = listUpdateCallback + mUpdateFinishedCallback = updateFinishedCallback + mConfig = config + } + + /** + * Pass a new List to the AdapterHelper. Adapter updates will be computed on a background + * thread. + * + * + * If a List is already present, a diff will be computed asynchronously on a background thread. + * When the diff is computed, it will be applied (dispatched to the [ListUpdateCallback]), + * and the new List will be swapped in. + * + * @param newList The new List. + */ + fun submitList(newList: List<T>?) { + val oldList = mList + + if (newList === oldList) { + // nothing to do + return + } + + // incrementing generation means any currently-running diffs are discarded when they finish + val runGeneration = ++mMaxScheduledGeneration + + // fast simple remove all + if (newList == null) { + val countRemoved = oldList?.size ?: 0 + mList = null + currentList = emptyList() + // notify last, after list is updated + mUpdateCallback.onRemoved(0, countRemoved) + return + } + + // fast simple first insert + if (oldList == null) { + mList = newList + currentList = Collections.unmodifiableList(newList) + // notify last, after list is updated + mUpdateCallback.onInserted(0, newList.size) + return + } + + mConfig.backgroundThreadExecutor.execute { + val result = DiffUtil.calculateDiff(object : DiffUtil.Callback() { + override fun getOldListSize(): Int { + return oldList.size + } + + override fun getNewListSize(): Int { + return newList.size + } + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return mConfig.diffCallback.areItemsTheSame( + oldList[oldItemPosition], newList[newItemPosition]) + } + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return mConfig.diffCallback.areContentsTheSame( + oldList[oldItemPosition], newList[newItemPosition]) + } + + override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? { + return mConfig.diffCallback.getChangePayload( + oldList[oldItemPosition], newList[newItemPosition]) + } + }) + + mConfig.mainThreadExecutor.execute { + if (mMaxScheduledGeneration == runGeneration) { + latchList(newList, result) + } + } + } + } + + private fun latchList(newList: List<T>, diffResult: DiffUtil.DiffResult) { + mList = newList + // notify last, after list is updated + currentList = Collections.unmodifiableList(newList) + diffResult.dispatchUpdatesTo(mUpdateCallback) + mUpdateFinishedCallback?.invoke(newList) + } +} diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/lists/ListAdapter.kt b/app/src/main/java/de/kuschku/quasseldroid/util/lists/ListAdapter.kt new file mode 100644 index 0000000000000000000000000000000000000000..55ffbf150f73d969e658c37c227fc031544573ea --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid/util/lists/ListAdapter.kt @@ -0,0 +1,133 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.kuschku.quasseldroid.util.lists + +import android.support.v7.util.AdapterListUpdateCallback +import android.support.v7.util.DiffUtil +import android.support.v7.widget.RecyclerView + +/** + * [RecyclerView.Adapter] base class for presenting List data in a + * [RecyclerView], including computing diffs between Lists on a background thread. + * + * + * This class is a convenience wrapper around [AsyncListDiffer] that implements Adapter common + * default behavior for item access and counting. + * + * + * While using a LiveData<List> is an easy way to provide data to the adapter, it isn't required + * - you can use [.submitList] when new lists are available. + * + * + * A complete usage pattern with Room would look like this: + * <pre> + * @Dao + * interface UserDao { + * @Query("SELECT * FROM user ORDER BY lastName ASC") + * public abstract LiveData<List<User>> usersByLastName(); + * } + * + * class MyViewModel extends ViewModel { + * public final LiveData<List<User>> usersList; + * public MyViewModel(UserDao userDao) { + * usersList = userDao.usersByLastName(); + * } + * } + * + * class MyActivity extends AppCompatActivity { + * @Override + * public void onCreate(Bundle savedState) { + * super.onCreate(savedState); + * MyViewModel viewModel = ViewModelProviders.of(this).get(MyViewModel.class); + * RecyclerView recyclerView = findViewById(R.id.user_list); + * UserAdapter<User> adapter = new UserAdapter(); + * viewModel.usersList.observe(this, list -> adapter.submitList(list)); + * recyclerView.setAdapter(adapter); + * } + * } + * + * class UserAdapter extends ListAdapter<User, UserViewHolder> { + * public UserAdapter() { + * super(User.DIFF_CALLBACK); + * } + * @Override + * public void onBindViewHolder(UserViewHolder holder, int position) { + * holder.bindTo(getItem(position)); + * } + * public static final DiffUtil.ItemCallback<User> DIFF_CALLBACK = + * new DiffUtil.ItemCallback<User>() { + * @Override + * public boolean areItemsTheSame( + * @NonNull User oldUser, @NonNull User newUser) { + * // User properties may have changed if reloaded from the DB, but ID is fixed + * return oldUser.getId() == newUser.getId(); + * } + * @Override + * public boolean areContentsTheSame( + * @NonNull User oldUser, @NonNull User newUser) { + * // NOTE: if you use equals, your object must properly override Object#equals() + * // Incorrectly returning false here will result in too many animations. + * return oldUser.equals(newUser); + * } + * } + * }</pre> + * + * Advanced users that wish for more control over adapter behavior, or to provide a specific base + * class should refer to [AsyncListDiffer], which provides custom mapping from diff events + * to adapter positions. + * + * @param <T> Type of the Lists this Adapter will receive. + * @param <VH> A class that extends ViewHolder that will be used by the adapter. +</VH></T> */ +abstract class ListAdapter<T, VH : RecyclerView.ViewHolder> : RecyclerView.Adapter<VH> { + private val mHelper: AsyncListDiffer<T> + + protected constructor(diffCallback: DiffUtil.ItemCallback<T>) { + mHelper = AsyncListDiffer(AdapterListUpdateCallback(this), + ::onUpdateFinished, + AsyncDifferConfig.Builder(diffCallback).build()) + } + + protected constructor(config: AsyncDifferConfig<T>) { + mHelper = AsyncListDiffer(AdapterListUpdateCallback(this), + ::onUpdateFinished, + config) + } + + /** + * Submits a new list to be diffed, and displayed. + * + * + * If a list is already being displayed, a diff will be computed on a background thread, which + * will dispatch Adapter.notifyItem events on the main thread. + * + * @param list The new list to be displayed. + */ + fun submitList(list: List<T>) { + mHelper.submitList(list) + } + + protected fun getItem(position: Int): T { + return mHelper.currentList[position] + } + + override fun getItemCount(): Int { + return mHelper.currentList.size + } + + open fun onUpdateFinished(list: List<T>) = Unit +} diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/service/BackendServiceConnection.kt b/app/src/main/java/de/kuschku/quasseldroid/util/service/BackendServiceConnection.kt index bda94b13164351e16a160f11d26f74362e677d95..30526fc8559e1bfa7b0f91a5d6bf44871e5b8721 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/util/service/BackendServiceConnection.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/util/service/BackendServiceConnection.kt @@ -19,10 +19,8 @@ package de.kuschku.quasseldroid.util.service -import android.arch.lifecycle.Lifecycle -import android.arch.lifecycle.LifecycleObserver +import android.arch.lifecycle.DefaultLifecycleObserver import android.arch.lifecycle.LifecycleOwner -import android.arch.lifecycle.OnLifecycleEvent import android.content.ComponentName import android.content.Context import android.content.Intent @@ -34,7 +32,7 @@ import de.kuschku.quasseldroid.service.QuasselBinder import de.kuschku.quasseldroid.service.QuasselService import io.reactivex.subjects.BehaviorSubject -class BackendServiceConnection : ServiceConnection, LifecycleObserver { +class BackendServiceConnection : ServiceConnection, DefaultLifecycleObserver { val backend: BehaviorSubject<Optional<Backend>> = BehaviorSubject.createDefault(Optional.empty()) var context: Context? = null @@ -87,12 +85,9 @@ class BackendServiceConnection : ServiceConnection, LifecycleObserver { } } - @OnLifecycleEvent(Lifecycle.Event.ON_CREATE) - fun onCreate(lifecycleOwner: LifecycleOwner) = start() + override fun onCreate(owner: LifecycleOwner) = start() - @OnLifecycleEvent(Lifecycle.Event.ON_START) - fun onStart(lifecycleOwner: LifecycleOwner) = bind() + override fun onStart(owner: LifecycleOwner) = bind() - @OnLifecycleEvent(Lifecycle.Event.ON_STOP) - fun onStop(lifecycleOwner: LifecycleOwner) = unbind() + override fun onStop(owner: LifecycleOwner) = unbind() } diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index d0cc17b265288a8ba6f6c692542e6c6ee4ef0a4e..5902242d3240f2828c363c82bbcb23be91402141 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -17,25 +17,6 @@ * with this program. If not, see <http://www.gnu.org/licenses/>. */ -/* - * Quasseldroid - Quassel client for Android - * - * Copyright (c) 2018 Janne Koschinski - * Copyright (c) 2018 The Quassel Project - * - * This program is free software: you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 3 as published - * by the Free Software Foundation. - * - * This program 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 this program. If not, see <http://www.gnu.org/licenses/>. - */ - import org.gradle.api.Project import org.gradle.api.artifacts.ExternalModuleDependency import org.gradle.kotlin.dsl.* diff --git a/lifecycle-ktx/build.gradle.kts b/lifecycle-ktx/build.gradle.kts new file mode 100644 index 0000000000000000000000000000000000000000..42b0627f20ed1cf1b927435d940b460ac43f85ce --- /dev/null +++ b/lifecycle-ktx/build.gradle.kts @@ -0,0 +1,41 @@ +/* + * Quasseldroid - Quassel client for Android + * + * Copyright (c) 2018 Janne Koschinski + * Copyright (c) 2018 The Quassel Project + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3 as published + * by the Free Software Foundation. + * + * This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import org.gradle.api.Project +import org.gradle.api.artifacts.ExternalModuleDependency +import org.gradle.kotlin.dsl.* +import org.jetbrains.kotlin.gradle.plugin.KaptExtension +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + kotlin("jvm") + kotlin("kapt") +} + +dependencies { + implementation(kotlin("stdlib", "1.2.41")) + + withVersion("27.1.1") { + implementation("com.android.support", "support-annotations", version) + } + + withVersion("1.1.1") { + implementation("android.arch.lifecycle", "common", version) + } +} diff --git a/lifecycle-ktx/src/main/java/android/arch/lifecycle/DefaultLifecycleObserver.kt b/lifecycle-ktx/src/main/java/android/arch/lifecycle/DefaultLifecycleObserver.kt new file mode 100644 index 0000000000000000000000000000000000000000..7ddd156ec9e4b9fbd187e9fb38eea08d2f2db1ba --- /dev/null +++ b/lifecycle-ktx/src/main/java/android/arch/lifecycle/DefaultLifecycleObserver.kt @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.arch.lifecycle + +/** + * Callback interface for listening to [LifecycleOwner] state changes. + * + * + * If you use Java 8 language, **always** prefer it over annotations. + */ +interface DefaultLifecycleObserver : FullLifecycleObserverProxy { + /** + * Notifies that `ON_CREATE` event occurred. + * + * + * This method will be called after the [LifecycleOwner]'s `onCreate` + * method returns. + * + * @param owner the component, whose state was changed + */ + override fun onCreate(owner: LifecycleOwner) = Unit + + /** + * Notifies that `ON_START` event occurred. + * + * + * This method will be called after the [LifecycleOwner]'s `onStart` method returns. + * + * @param owner the component, whose state was changed + */ + override fun onStart(owner: LifecycleOwner) = Unit + + /** + * Notifies that `ON_RESUME` event occurred. + * + * + * This method will be called after the [LifecycleOwner]'s `onResume` + * method returns. + * + * @param owner the component, whose state was changed + */ + override fun onResume(owner: LifecycleOwner) = Unit + + /** + * Notifies that `ON_PAUSE` event occurred. + * + * + * This method will be called before the [LifecycleOwner]'s `onPause` method + * is called. + * + * @param owner the component, whose state was changed + */ + override fun onPause(owner: LifecycleOwner) = Unit + + /** + * Notifies that `ON_STOP` event occurred. + * + * + * This method will be called before the [LifecycleOwner]'s `onStop` method + * is called. + * + * @param owner the component, whose state was changed + */ + override fun onStop(owner: LifecycleOwner) = Unit + + /** + * Notifies that `ON_DESTROY` event occurred. + * + * + * This method will be called before the [LifecycleOwner]'s `onStop` method + * is called. + * + * @param owner the component, whose state was changed + */ + override fun onDestroy(owner: LifecycleOwner) = Unit +} diff --git a/lifecycle-ktx/src/main/java/android/arch/lifecycle/FullLifecycleObserverProxy.java b/lifecycle-ktx/src/main/java/android/arch/lifecycle/FullLifecycleObserverProxy.java new file mode 100644 index 0000000000000000000000000000000000000000..82990c78449e47298563fc19d6387b5e701e3d34 --- /dev/null +++ b/lifecycle-ktx/src/main/java/android/arch/lifecycle/FullLifecycleObserverProxy.java @@ -0,0 +1,23 @@ +/* + * Quasseldroid - Quassel client for Android + * + * Copyright (c) 2018 Janne Koschinski + * Copyright (c) 2018 The Quassel Project + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3 as published + * by the Free Software Foundation. + * + * This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package android.arch.lifecycle; + +public interface FullLifecycleObserverProxy extends FullLifecycleObserver { +} diff --git a/settings.gradle b/settings.gradle index eba2c7c83d29b370f8ef4e6730edf7ff248c253a..68b525eeb86ce580f54b645b2505c00848b0ced0 100644 --- a/settings.gradle +++ b/settings.gradle @@ -17,7 +17,7 @@ * with this program. If not, see <http://www.gnu.org/licenses/>. */ -include ':invokerannotations', +include ':invokerannotations', ':lifecycle-ktx', ':invokergenerator', ':lib', ":viewmodel", diff --git a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/QuasselViewModel.kt b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/QuasselViewModel.kt index 3b802adc72bfefb7cee67f75863e171b79e3c8b3..a16314f81d0309c1cc6a9ee79b5305dd73d2cd8d 100644 --- a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/QuasselViewModel.kt +++ b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/QuasselViewModel.kt @@ -42,6 +42,7 @@ import io.reactivex.BackpressureStrategy import io.reactivex.Flowable import io.reactivex.Observable import io.reactivex.subjects.BehaviorSubject +import io.reactivex.subjects.PublishSubject import java.util.concurrent.TimeUnit class QuasselViewModel : ViewModel() { @@ -58,6 +59,7 @@ class QuasselViewModel : ViewModel() { val expandedMessages = BehaviorSubject.createDefault(emptySet<MsgId>()) val buffer = BehaviorSubject.createDefault(Int.MAX_VALUE) + val bufferOpened = PublishSubject.create<Unit>() val bufferViewConfigId = BehaviorSubject.createDefault(Int.MAX_VALUE)