Lots of progress on the conversations list view

Signed-off-by: Mario Danic <mario@lovelyhq.com>
This commit is contained in:
Mario Danic 2019-10-16 13:42:11 +02:00
parent ab3cb83cdf
commit 8a3008ef25
45 changed files with 890 additions and 197 deletions

View File

@ -76,6 +76,10 @@ android {
] ]
} }
} }
dataBinding {
enabled = true
}
} }
dexOptions { dexOptions {
@ -136,8 +140,9 @@ android {
} }
ext { ext {
workVersion = "1.0.1" work_version = "1.0.1"
koin_version = "2.0.1" koin_version = "2.1.0-alpha-1"
lifecycle_version = "2.1.0"
} }
@ -161,6 +166,25 @@ dependencies {
implementation "org.koin:koin-androidx-ext:$koin_version" implementation "org.koin:koin-androidx-ext:$koin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2'
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
implementation "com.github.stateless4j:stateless4j:2.6.0"
// ViewModel and LiveData
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0-beta01"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-beta01"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-beta01"
// optional - ReactiveStreams support for LiveData
implementation "androidx.lifecycle:lifecycle-reactivestreams:$lifecycle_version" // For Kotlin use lifecycle-reactivestreams-ktx
// optional - Test helpers for LiveData
testImplementation "androidx.arch.core:core-testing:$lifecycle_version"
implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'com.google.android.material:material:1.1.0-beta01' implementation 'com.google.android.material:material:1.1.0-beta01'
@ -168,10 +192,10 @@ dependencies {
implementation 'com.github.vanniktech:Emoji:0.6.0' implementation 'com.github.vanniktech:Emoji:0.6.0'
implementation group: 'androidx.emoji', name: 'emoji-bundled', version: '1.0.0' implementation group: 'androidx.emoji', name: 'emoji-bundled', version: '1.0.0'
implementation 'org.michaelevans.colorart:library:0.0.3' implementation 'org.michaelevans.colorart:library:0.0.3'
implementation "android.arch.work:work-runtime:${workVersion}" implementation "android.arch.work:work-runtime:${work_version}"
implementation "android.arch.work:work-rxjava2:${workVersion}" implementation "android.arch.work:work-rxjava2:${work_version}"
implementation 'com.google.android:flexbox:1.1.0' implementation 'com.google.android:flexbox:1.1.0'
androidTestImplementation "android.arch.work:work-testing:${workVersion}" androidTestImplementation "android.arch.work:work-testing:${work_version}"
implementation ('com.gitlab.bitfireAT:dav4jvm:f2078bc846', { implementation ('com.gitlab.bitfireAT:dav4jvm:f2078bc846', {
exclude group: 'org.ogce', module: 'xpp3' // Android comes with its own XmlPullParser exclude group: 'org.ogce', module: 'xpp3' // Android comes with its own XmlPullParser
}) })
@ -179,7 +203,7 @@ dependencies {
implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
implementation 'androidx.biometric:biometric:1.0.0-beta02' implementation 'androidx.biometric:biometric:1.0.0-rc01'
implementation "androidx.lifecycle:lifecycle-extensions:2.1.0" implementation "androidx.lifecycle:lifecycle-extensions:2.1.0"
implementation 'androidx.multidex:multidex:2.0.1' implementation 'androidx.multidex:multidex:2.0.1'
@ -192,6 +216,7 @@ dependencies {
implementation 'com.bluelinelabs:conductor-archlifecycle:3.0.0-rc2' implementation 'com.bluelinelabs:conductor-archlifecycle:3.0.0-rc2'
implementation 'com.bluelinelabs:conductor-rxlifecycle2:3.0.0-rc2' implementation 'com.bluelinelabs:conductor-rxlifecycle2:3.0.0-rc2'
implementation 'com.bluelinelabs:conductor-autodispose:3.0.0-rc2' implementation 'com.bluelinelabs:conductor-autodispose:3.0.0-rc2'
implementation "com.github.miquelbeltran:conductor-viewmodel:1.0.3"
implementation 'com.squareup.okhttp3:okhttp:4.2.2' implementation 'com.squareup.okhttp3:okhttp:4.2.2'
implementation 'com.squareup.okhttp3:okhttp-urlconnection:4.2.2' implementation 'com.squareup.okhttp3:okhttp-urlconnection:4.2.2'
@ -226,6 +251,7 @@ dependencies {
implementation 'com.github.HITGIF:TextFieldBoxes:1.4.5' implementation 'com.github.HITGIF:TextFieldBoxes:1.4.5'
implementation 'eu.davidea:flexible-adapter:5.1.0' implementation 'eu.davidea:flexible-adapter:5.1.0'
implementation 'eu.davidea:flexible-adapter-ui:1.0.0' implementation 'eu.davidea:flexible-adapter-ui:1.0.0'
implementation 'eu.davidea:flexible-adapter-livedata:1.0.0-b3'
implementation 'org.webrtc:google-webrtc:1.0.23295' implementation 'org.webrtc:google-webrtc:1.0.23295'
implementation 'com.yarolegovich:lovely-dialog:1.1.0' implementation 'com.yarolegovich:lovely-dialog:1.1.0'
implementation 'com.yarolegovich:lovelyinput:1.0.9' implementation 'com.yarolegovich:lovelyinput:1.0.9'

View File

@ -43,6 +43,7 @@ import com.nextcloud.talk.controllers.ConversationsListController
import com.nextcloud.talk.controllers.LockedController import com.nextcloud.talk.controllers.LockedController
import com.nextcloud.talk.controllers.ServerSelectionController import com.nextcloud.talk.controllers.ServerSelectionController
import com.nextcloud.talk.controllers.base.providers.ActionBarProvider import com.nextcloud.talk.controllers.base.providers.ActionBarProvider
import com.nextcloud.talk.newarch.features.conversationsList.ConversationsListView
import com.nextcloud.talk.utils.ConductorRemapping import com.nextcloud.talk.utils.ConductorRemapping
import com.nextcloud.talk.utils.SecurityUtils import com.nextcloud.talk.utils.SecurityUtils
import com.nextcloud.talk.utils.bundle.BundleKeys import com.nextcloud.talk.utils.bundle.BundleKeys
@ -91,7 +92,7 @@ class MainActivity : BaseActivity(), ActionBarProvider {
if (intent.hasExtra(BundleKeys.KEY_FROM_NOTIFICATION_START_CALL)) { if (intent.hasExtra(BundleKeys.KEY_FROM_NOTIFICATION_START_CALL)) {
if (!router!!.hasRootController()) { if (!router!!.hasRootController()) {
router!!.setRoot(RouterTransaction.with(ConversationsListController()) router!!.setRoot(RouterTransaction.with(ConversationsListView())
.pushChangeHandler(HorizontalChangeHandler()) .pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler())) .popChangeHandler(HorizontalChangeHandler()))
} }
@ -99,7 +100,7 @@ class MainActivity : BaseActivity(), ActionBarProvider {
} else if (!router!!.hasRootController()) { } else if (!router!!.hasRootController()) {
if (hasDb) { if (hasDb) {
if (userUtils.anyUserExists()) { if (userUtils.anyUserExists()) {
router!!.setRoot(RouterTransaction.with(ConversationsListController()) router!!.setRoot(RouterTransaction.with(ConversationsListView())
.pushChangeHandler(HorizontalChangeHandler()) .pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler())) .popChangeHandler(HorizontalChangeHandler()))
} else { } else {

View File

@ -98,6 +98,7 @@ public class ConversationItem extends AbstractFlexibleItem<ConversationItem.Conv
public void bindViewHolder(FlexibleAdapter<IFlexible> adapter, ConversationItemViewHolder holder, int position, List<Object> payloads) { public void bindViewHolder(FlexibleAdapter<IFlexible> adapter, ConversationItemViewHolder holder, int position, List<Object> payloads) {
Context appContext = Context appContext =
NextcloudTalkApplication.Companion.getSharedApplication().getApplicationContext(); NextcloudTalkApplication.Companion.getSharedApplication().getApplicationContext();
holder.dialogAvatar.setController(null); holder.dialogAvatar.setController(null);
if (adapter.hasFilter()) { if (adapter.hasFilter()) {

View File

@ -319,7 +319,7 @@ public interface NcApi {
@FormUrlEncoded @FormUrlEncoded
@PUT @PUT
Observable<GenericOverall> setReadOnlyState(@Header("Authorization") String authorization, @Url String url, @Field("state") int state); Observable<GenericOverall> setReadOnlyState(@Header("Authorization") String authorization, @Url String url, @Field("viewState") int state);
@FormUrlEncoded @FormUrlEncoded
@ -332,7 +332,7 @@ public interface NcApi {
@FormUrlEncoded @FormUrlEncoded
@PUT @PUT
Observable<GenericOverall> setLobbyForConversation(@Header("Authorization") String authorization, Observable<GenericOverall> setLobbyForConversation(@Header("Authorization") String authorization,
@Url String url, @Field("state") Integer state, @Url String url, @Field("viewState") Integer state,
@Field("timer") Long timer); @Field("timer") Long timer);
} }

View File

@ -52,6 +52,7 @@ import com.nextcloud.talk.jobs.SignalingSettingsWorker
import com.nextcloud.talk.newarch.di.module.CommunicationModule import com.nextcloud.talk.newarch.di.module.CommunicationModule
import com.nextcloud.talk.newarch.di.module.NetworkModule import com.nextcloud.talk.newarch.di.module.NetworkModule
import com.nextcloud.talk.newarch.di.module.StorageModule import com.nextcloud.talk.newarch.di.module.StorageModule
import com.nextcloud.talk.newarch.features.conversationsList.di.module.ConversationsListModule
import com.nextcloud.talk.utils.ClosedInterfaceImpl import com.nextcloud.talk.utils.ClosedInterfaceImpl
import com.nextcloud.talk.utils.DeviceUtils import com.nextcloud.talk.utils.DeviceUtils
import com.nextcloud.talk.utils.DisplayUtils import com.nextcloud.talk.utils.DisplayUtils
@ -193,7 +194,7 @@ class NextcloudTalkApplication : Application(), LifecycleObserver {
startKoin { startKoin {
androidContext(this@NextcloudTalkApplication) androidContext(this@NextcloudTalkApplication)
androidLogger() androidLogger()
modules(listOf(CommunicationModule, StorageModule, NetworkModule)) modules(listOf(CommunicationModule, StorageModule, NetworkModule, ConversationsListModule))
} }
} }

View File

@ -75,7 +75,7 @@ public class BrowserController extends BaseController implements ListingInterfac
private final Set<String> selectedPaths; private final Set<String> selectedPaths;
@Inject @Inject
UserUtils userUtils; UserUtils userUtils;
@BindView(R.id.recycler_view) @BindView(R.id.recyclerView)
RecyclerView recyclerView; RecyclerView recyclerView;
@BindView(R.id.fast_scroller) @BindView(R.id.fast_scroller)
FastScroller fastScroller; FastScroller fastScroller;

View File

@ -84,6 +84,9 @@ public class AccountVerificationController extends BaseController {
@Inject @Inject
AppPreferences appPreferences; AppPreferences appPreferences;
@Inject
EventBus eventBus;
@BindView(R.id.progress_text) @BindView(R.id.progress_text)
TextView progressText; TextView progressText;
@ -115,6 +118,18 @@ public class AccountVerificationController extends BaseController {
return inflater.inflate(R.layout.controller_account_verification, container, false); return inflater.inflate(R.layout.controller_account_verification, container, false);
} }
@Override
protected void onDetach(@NonNull View view) {
eventBus.unregister(this);
super.onDetach(view);
}
@Override
protected void onAttach(@NonNull View view) {
super.onAttach(view);
eventBus.register(this);
}
@Override @Override
protected void onViewBound(@NonNull View view) { protected void onViewBound(@NonNull View view) {
super.onViewBound(view); super.onViewBound(view);

View File

@ -197,6 +197,8 @@ public class CallController extends BaseController {
AppPreferences appPreferences; AppPreferences appPreferences;
@Inject @Inject
Cache cache; Cache cache;
@Inject
EventBus eventBus;
private PeerConnectionFactory peerConnectionFactory; private PeerConnectionFactory peerConnectionFactory;
private MediaConstraints audioConstraints; private MediaConstraints audioConstraints;
@ -1239,6 +1241,18 @@ public class CallController extends BaseController {
} }
} }
@Override
protected void onDetach(@NonNull View view) {
eventBus.unregister(this);
super.onDetach(view);
}
@Override
protected void onAttach(@NonNull View view) {
super.onAttach(view);
eventBus.register(this);
}
@Subscribe(threadMode = ThreadMode.BACKGROUND) @Subscribe(threadMode = ThreadMode.BACKGROUND)
public void onMessageEvent(WebSocketCommunicationEvent webSocketCommunicationEvent) { public void onMessageEvent(WebSocketCommunicationEvent webSocketCommunicationEvent) {
switch (webSocketCommunicationEvent.getType()) { switch (webSocketCommunicationEvent.getType()) {

View File

@ -160,6 +160,18 @@ public class CallNotificationController extends BaseController {
return inflater.inflate(R.layout.controller_call_notification, container, false); return inflater.inflate(R.layout.controller_call_notification, container, false);
} }
@Override
protected void onDetach(@NonNull View view) {
eventBus.unregister(this);
super.onDetach(view);
}
@Override
protected void onAttach(@NonNull View view) {
super.onAttach(view);
eventBus.register(this);
}
private void showAnswerControls() { private void showAnswerControls() {
callAnswerCameraView.setVisibility(View.VISIBLE); callAnswerCameraView.setVisibility(View.VISIBLE);
callAnswerVoiceOnlyView.setVisibility(View.VISIBLE); callAnswerVoiceOnlyView.setVisibility(View.VISIBLE);

View File

@ -654,6 +654,7 @@ class ChatController(args: Bundle) : BaseController(), MessagesListAdapter
override fun onAttach(view: View) { override fun onAttach(view: View) {
super.onAttach(view) super.onAttach(view)
eventBus.register(this)
if (conversationUser?.userId != "?" && conversationUser?.hasSpreedFeatureCapability( if (conversationUser?.userId != "?" && conversationUser?.hasSpreedFeatureCapability(
"mention-flag" "mention-flag"
@ -728,7 +729,7 @@ class ChatController(args: Bundle) : BaseController(), MessagesListAdapter
} }
override fun onDetach(view: View) { override fun onDetach(view: View) {
super.onDetach(view) eventBus.unregister(this)
ApplicationWideCurrentRoomHolder.getInstance() ApplicationWideCurrentRoomHolder.getInstance()
.clear() .clear()
@ -747,6 +748,8 @@ class ChatController(args: Bundle) : BaseController(), MessagesListAdapter
if (mentionAutocomplete != null && mentionAutocomplete!!.isPopupShowing) { if (mentionAutocomplete != null && mentionAutocomplete!!.isPopupShowing) {
mentionAutocomplete?.dismissPopup() mentionAutocomplete?.dismissPopup()
} }
super.onDetach(view)
} }
override fun getTitle(): String? { override fun getTitle(): String? {

View File

@ -128,7 +128,7 @@ public class ContactsController extends BaseController implements SearchView.OnQ
AppPreferences appPreferences; AppPreferences appPreferences;
@BindView(R.id.progressBar) @BindView(R.id.progressBar)
ProgressBar progressBar; ProgressBar progressBar;
@BindView(R.id.recycler_view) @BindView(R.id.recyclerView)
RecyclerView recyclerView; RecyclerView recyclerView;
@BindView(R.id.swipe_refresh_layout) @BindView(R.id.swipe_refresh_layout)
@ -210,9 +210,16 @@ public class ContactsController extends BaseController implements SearchView.OnQ
return inflater.inflate(R.layout.controller_contacts_rv, container, false); return inflater.inflate(R.layout.controller_contacts_rv, container, false);
} }
@Override
protected void onDetach(@NonNull View view) {
eventBus.unregister(this);
super.onDetach(view);
}
@Override @Override
protected void onAttach(@NonNull View view) { protected void onAttach(@NonNull View view) {
super.onAttach(view); super.onAttach(view);
eventBus.register(this);
if (isNewConversationView) { if (isNewConversationView) {
toggleNewCallHeaderVisibility(!isPublicCall); toggleNewCallHeaderVisibility(!isPublicCall);

View File

@ -20,7 +20,7 @@
package com.nextcloud.talk.controllers package com.nextcloud.talk.controllers
import android.content.Context import android.content.res.Configuration
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable import android.graphics.drawable.LayerDrawable
import android.os.Bundle import android.os.Bundle
@ -85,7 +85,6 @@ import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode import org.greenrobot.eventbus.ThreadMode
import java.util.ArrayList import java.util.ArrayList
@ -94,7 +93,6 @@ import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class) @AutoInjector(NextcloudTalkApplication::class)
class ConversationInfoController(args: Bundle) : BaseController(), FlexibleAdapter.OnItemClickListener { class ConversationInfoController(args: Bundle) : BaseController(), FlexibleAdapter.OnItemClickListener {
@BindView(R.id.notification_settings) @BindView(R.id.notification_settings)
lateinit var notificationsPreferenceScreen: MaterialPreferenceScreen lateinit var notificationsPreferenceScreen: MaterialPreferenceScreen
@BindView(R.id.progressBar) @BindView(R.id.progressBar)
@ -115,7 +113,7 @@ class ConversationInfoController(args: Bundle) : BaseController(), FlexibleAdapt
lateinit var conversationDisplayName: EmojiTextView lateinit var conversationDisplayName: EmojiTextView
@BindView(R.id.participants_list_category) @BindView(R.id.participants_list_category)
lateinit var participantsListCategory: MaterialPreferenceCategoryWithRightLink lateinit var participantsListCategory: MaterialPreferenceCategoryWithRightLink
@BindView(R.id.recycler_view) @BindView(R.id.recyclerView)
lateinit var recyclerView: RecyclerView lateinit var recyclerView: RecyclerView
@BindView(R.id.deleteConversationAction) @BindView(R.id.deleteConversationAction)
lateinit var deleteConversationAction: MaterialStandardPreference lateinit var deleteConversationAction: MaterialStandardPreference
@ -179,8 +177,14 @@ class ConversationInfoController(args: Bundle) : BaseController(), FlexibleAdapt
return inflater.inflate(R.layout.controller_conversation_info, container, false) return inflater.inflate(R.layout.controller_conversation_info, container, false)
} }
override fun onDetach(view: View) {
eventBus.unregister(this)
super.onDetach(view)
}
override fun onAttach(view: View) { override fun onAttach(view: View) {
super.onAttach(view) super.onAttach(view)
eventBus.register(this)
if (databaseStorageModule == null) { if (databaseStorageModule == null) {
databaseStorageModule = DatabaseStorageModule(conversationUser!!, conversationToken) databaseStorageModule = DatabaseStorageModule(conversationUser!!, conversationToken)
@ -327,7 +331,7 @@ class ConversationInfoController(args: Bundle) : BaseController(), FlexibleAdapt
super.onRestoreViewState(view, savedViewState) super.onRestoreViewState(view, savedViewState)
if (LovelySaveStateHandler.wasDialogOnScreen(savedViewState)) { if (LovelySaveStateHandler.wasDialogOnScreen(savedViewState)) {
//Dialog won't be restarted automatically, so we need to call this method. //Dialog won't be restarted automatically, so we need to call this method.
//Each dialog knows how to restore its state //Each dialog knows how to restore its viewState
showLovelyDialog(LovelySaveStateHandler.getSavedDialogId(savedViewState), savedViewState) showLovelyDialog(LovelySaveStateHandler.getSavedDialogId(savedViewState), savedViewState)
} }
} }

View File

@ -133,7 +133,7 @@ public class ConversationsListController extends BaseController implements Searc
@Inject @Inject
AppPreferences appPreferences; AppPreferences appPreferences;
@BindView(R.id.recycler_view) @BindView(R.id.recyclerView)
RecyclerView recyclerView; RecyclerView recyclerView;
@BindView(R.id.swipeRefreshLayoutView) @BindView(R.id.swipeRefreshLayoutView)
@ -480,7 +480,7 @@ public class ConversationsListController extends BaseController implements Searc
searchQuery = savedViewState.getString(KEY_SEARCH_QUERY, ""); searchQuery = savedViewState.getString(KEY_SEARCH_QUERY, "");
if (LovelySaveStateHandler.wasDialogOnScreen(savedViewState)) { if (LovelySaveStateHandler.wasDialogOnScreen(savedViewState)) {
//Dialog won't be restarted automatically, so we need to call this method. //Dialog won't be restarted automatically, so we need to call this method.
//Each dialog knows how to restore its state //Each dialog knows how to restore its viewState
showLovelyDialog(LovelySaveStateHandler.getSavedDialogId(savedViewState), savedViewState); showLovelyDialog(LovelySaveStateHandler.getSavedDialogId(savedViewState), savedViewState);
} }
} }

View File

@ -61,7 +61,7 @@ public class RingtoneSelectionController extends BaseController implements Flexi
private static final String TAG = "RingtoneSelectionController"; private static final String TAG = "RingtoneSelectionController";
@BindView(R.id.recycler_view) @BindView(R.id.recyclerView)
RecyclerView recyclerView; RecyclerView recyclerView;
@BindView(R.id.swipe_refresh_layout) @BindView(R.id.swipe_refresh_layout)

View File

@ -346,7 +346,7 @@ public class SettingsController extends BaseController {
super.onRestoreViewState(view, savedViewState); super.onRestoreViewState(view, savedViewState);
if (LovelySaveStateHandler.wasDialogOnScreen(savedViewState)) { if (LovelySaveStateHandler.wasDialogOnScreen(savedViewState)) {
//Dialog won't be restarted automatically, so we need to call this method. //Dialog won't be restarted automatically, so we need to call this method.
//Each dialog knows how to restore its state //Each dialog knows how to restore its viewState
showLovelyDialog(LovelySaveStateHandler.getSavedDialogId(savedViewState), savedViewState); showLovelyDialog(LovelySaveStateHandler.getSavedDialogId(savedViewState), savedViewState);
} }
} }

View File

@ -64,7 +64,7 @@ public class SwitchAccountController extends BaseController {
@Inject @Inject
UserUtils userUtils; UserUtils userUtils;
@BindView(R.id.recycler_view) @BindView(R.id.recyclerView)
RecyclerView recyclerView; RecyclerView recyclerView;
@Inject @Inject

View File

@ -21,10 +21,11 @@
*/ */
package com.nextcloud.talk.controllers.base package com.nextcloud.talk.controllers.base
import android.content.ComponentCallbacks
import android.content.Context import android.content.Context
import android.content.res.Configuration
import android.os.Build import android.os.Build
import android.util.Log import android.util.Log
import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -44,16 +45,11 @@ import com.nextcloud.talk.controllers.base.providers.ActionBarProvider
import com.nextcloud.talk.utils.preferences.AppPreferences import com.nextcloud.talk.utils.preferences.AppPreferences
import com.uber.autodispose.lifecycle.LifecycleScopeProvider import com.uber.autodispose.lifecycle.LifecycleScopeProvider
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.koin.core.Koin import org.koin.android.ext.android.inject
import org.koin.core.KoinComponent
import org.koin.core.inject
import org.koin.core.qualifier.named
import org.koin.core.scope.Scope
import java.util.ArrayList import java.util.ArrayList
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class) @AutoInjector(NextcloudTalkApplication::class)
abstract class BaseController : ButterKnifeController(), KoinComponent { abstract class BaseController : ButterKnifeController(), ComponentCallbacks {
val scopeProvider: LifecycleScopeProvider<*> = ControllerScopeProvider.from(this) val scopeProvider: LifecycleScopeProvider<*> = ControllerScopeProvider.from(this)
@ -75,10 +71,6 @@ abstract class BaseController : ButterKnifeController(), KoinComponent {
return actionBarProvider?.supportActionBar return actionBarProvider?.supportActionBar
} }
override fun getKoin(): Koin {
return super.getKoin()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
android.R.id.home -> { android.R.id.home -> {
@ -112,7 +104,6 @@ abstract class BaseController : ButterKnifeController(), KoinComponent {
override fun onAttach(view: View) { override fun onAttach(view: View) {
super.onAttach(view) super.onAttach(view)
eventBus.register(this)
setTitle() setTitle()
if (actionBar != null) { if (actionBar != null) {
@ -121,7 +112,6 @@ abstract class BaseController : ButterKnifeController(), KoinComponent {
} }
override fun onDetach(view: View) { override fun onDetach(view: View) {
eventBus.unregister(this)
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(view.windowToken, 0) imm.hideSoftInputFromWindow(view.windowToken, 0)
super.onDetach(view) super.onDetach(view)
@ -159,6 +149,12 @@ abstract class BaseController : ButterKnifeController(), KoinComponent {
} }
} }
override fun onLowMemory() {
}
override fun onConfigurationChanged(newConfig: Configuration) {
}
companion object { companion object {
private val TAG = "BaseController" private val TAG = "BaseController"

View File

@ -31,7 +31,7 @@ import com.bluelinelabs.conductor.Controller
abstract class ButterKnifeController : Controller { abstract class ButterKnifeController : Controller {
private var unbinder: Unbinder? = null protected var unbinder: Unbinder? = null
constructor() {} constructor() {}

View File

@ -67,7 +67,7 @@ import org.parceler.Parcels;
@AutoInjector(NextcloudTalkApplication.class) @AutoInjector(NextcloudTalkApplication.class)
public class CallMenuController extends BaseController public class CallMenuController extends BaseController
implements FlexibleAdapter.OnItemClickListener { implements FlexibleAdapter.OnItemClickListener {
@BindView(R.id.recycler_view) @BindView(R.id.recyclerView)
RecyclerView recyclerView; RecyclerView recyclerView;
@Inject @Inject

View File

@ -27,7 +27,7 @@ import com.nextcloud.talk.newarch.domain.repository.NextcloudTalkRepository
import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.ApiUtils
class NextcloudTalkRepositoryImpl(private val apiService: ApiService) : NextcloudTalkRepository { class NextcloudTalkRepositoryImpl(private val apiService: ApiService) : NextcloudTalkRepository {
override suspend fun getConversations(user: UserEntity): List<Conversation> { override suspend fun getConversationsForUser(user: UserEntity): List<Conversation> {
return apiService.getConversations(ApiUtils.getCredentials(user.username, user.token), return apiService.getConversations(ApiUtils.getCredentials(user.username, user.token),
ApiUtils.getUrlForGetRooms(user.baseUrl)).ocs.data ApiUtils.getUrlForGetRooms(user.baseUrl)).ocs.data
} }

View File

@ -50,6 +50,7 @@ import okhttp3.OkHttpClient
import okhttp3.internal.tls.OkHostnameVerifier import okhttp3.internal.tls.OkHostnameVerifier
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
import okhttp3.logging.HttpLoggingInterceptor.Logger import okhttp3.logging.HttpLoggingInterceptor.Logger
import org.koin.android.ext.koin.androidApplication
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module import org.koin.dsl.module
import retrofit2.Retrofit import retrofit2.Retrofit
@ -70,8 +71,16 @@ import javax.net.ssl.X509KeyManager
val NetworkModule = module { val NetworkModule = module {
single { createService(get()) } single { createService(get()) }
single { createRetrofit(get()) } single { createRetrofit(get()) }
single { createProxy(get()) }
single { createTrustManager() }
single { createCookieManager() }
single { createDispatcher() }
single { createKeyManager(get(), get()) }
single { createSslSocketFactory(get(), get()) }
single { createCache(androidApplication() as NextcloudTalkApplication) }
single { createOkHttpClient(androidContext(), get(), get(), get(), get(), get(), get(), get()) } single { createOkHttpClient(androidContext(), get(), get(), get(), get(), get(), get(), get()) }
factory { createGetConversationsUseCase(get(), get()) }
single { createNextcloudTalkRepository(get()) }
} }
fun createCookieManager(): CookieManager { fun createCookieManager(): CookieManager {
@ -242,9 +251,3 @@ fun createNextcloudTalkRepository(apiService: ApiService): NextcloudTalkReposito
return NextcloudTalkRepositoryImpl(apiService) return NextcloudTalkRepositoryImpl(apiService)
} }
fun createGetConversationsUseCase(
nextcloudTalkRepository: NextcloudTalkRepository,
apiErrorHandler: ApiErrorHandler
): GetConversationsUseCase {
return GetConversationsUseCase(nextcloudTalkRepository, apiErrorHandler)
}

View File

@ -24,5 +24,5 @@ import com.nextcloud.talk.models.database.UserEntity
import com.nextcloud.talk.models.json.conversations.Conversation import com.nextcloud.talk.models.json.conversations.Conversation
interface NextcloudTalkRepository { interface NextcloudTalkRepository {
suspend fun getConversations(user: UserEntity): List<Conversation> suspend fun getConversationsForUser(user: UserEntity): List<Conversation>
} }

View File

@ -31,6 +31,6 @@ class GetConversationsUseCase constructor(
) : UseCase<List<Conversation>, Any?>(apiErrorHandler) { ) : UseCase<List<Conversation>, Any?>(apiErrorHandler) {
override suspend fun run(params: Any?): List<Conversation> { override suspend fun run(params: Any?): List<Conversation> {
return nextcloudTalkRepository.getConversations(user); return nextcloudTalkRepository.getConversationsForUser(user);
} }
} }

View File

@ -31,6 +31,7 @@ abstract class UseCase<Type, in Params>(private val apiErrorHandler: ApiErrorHan
abstract suspend fun run(params: Params? = null): Type abstract suspend fun run(params: Params? = null): Type
lateinit var user: UserEntity lateinit var user: UserEntity
fun isUserInitialized() = ::user.isInitialized
fun invoke( fun invoke(
scope: CoroutineScope, scope: CoroutineScope,

View File

@ -18,29 +18,19 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package com.nextcloud.talk.newarch.conversationsList.mvp package com.nextcloud.talk.newarch.features.conversationsList
import android.view.View import androidx.lifecycle.ViewModel
import androidx.annotation.LayoutRes import androidx.lifecycle.ViewModelProvider
import autodagger.AutoInjector import com.nextcloud.talk.newarch.domain.usecases.GetConversationsUseCase
import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.utils.database.user.UserUtils
import com.nextcloud.talk.controllers.base.BaseController
@AutoInjector(NextcloudTalkApplication::class) class ConversationListViewModelFactory constructor(
abstract class BaseView : BaseController() { private val conversationsUseCase: GetConversationsUseCase,
private val userUtils: UserUtils
): ViewModelProvider.Factory {
override fun onDetach(view: View) { override fun <T : ViewModel?> create(modelClass: Class<T>): T {
super.onDetach(view) return ConversationsListViewModel(conversationsUseCase, userUtils) as T;
getPresenter().stop()
} }
override fun onDestroy() {
getPresenter().destroy()
super.onDestroy()
}
@LayoutRes
protected abstract fun getLayoutId(): Int
protected abstract fun getPresenter(): MvpPresenter
} }

View File

@ -0,0 +1,335 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* Copyright (C) 2017-2019 Mario Danic <mario@lovelyhq.com>
*
* This program 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.
*
* 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 com.nextcloud.talk.newarch.features.conversationsList
import android.app.SearchManager
import android.content.Context
import android.graphics.Bitmap
import android.os.Build
import android.os.Bundle
import android.text.InputType
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.SearchView.OnQueryTextListener
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
import androidx.core.view.MenuItemCompat
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import butterknife.OnClick
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
import com.bluelinelabs.conductor.changehandler.TransitionChangeHandlerCompat
import com.bluelinelabs.conductor.changehandler.VerticalChangeHandler
import com.facebook.common.executors.UiThreadImmediateExecutorService
import com.facebook.common.references.CloseableReference
import com.facebook.datasource.DataSource
import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber
import com.facebook.imagepipeline.image.CloseableImage
import com.nextcloud.talk.R
import com.nextcloud.talk.adapters.items.ConversationItem
import com.nextcloud.talk.controllers.ContactsController
import com.nextcloud.talk.controllers.SettingsController
import com.nextcloud.talk.controllers.bottomsheet.CallMenuController.MenuType
import com.nextcloud.talk.controllers.bottomsheet.CallMenuController.MenuType.REGULAR
import com.nextcloud.talk.models.json.conversations.Conversation
import com.nextcloud.talk.newarch.conversationsList.mvp.BaseView
import com.nextcloud.talk.newarch.mvvm.ViewState.FAILED
import com.nextcloud.talk.newarch.mvvm.ViewState.LOADED
import com.nextcloud.talk.newarch.mvvm.ViewState.LOADING
import com.nextcloud.talk.newarch.mvvm.ext.initRecyclerView
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.ConductorRemapping
import com.nextcloud.talk.utils.DisplayUtils
import com.nextcloud.talk.utils.animations.SharedElementTransition
import com.nextcloud.talk.utils.bundle.BundleKeys
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.FlexibleAdapter.OnItemClickListener
import eu.davidea.flexibleadapter.FlexibleAdapter.OnItemLongClickListener
import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager
import eu.davidea.flexibleadapter.items.IFlexible
import kotlinx.android.synthetic.main.controller_conversations_rv.view.emptyLayout
import kotlinx.android.synthetic.main.controller_conversations_rv.view.floatingActionButton
import kotlinx.android.synthetic.main.controller_conversations_rv.view.progressBar
import kotlinx.android.synthetic.main.controller_conversations_rv.view.recyclerView
import kotlinx.android.synthetic.main.controller_conversations_rv.view.swipeRefreshLayoutView
import kotlinx.android.synthetic.main.fast_scroller.view.fast_scroller
import org.koin.android.ext.android.inject
import org.parceler.Parcels
import java.util.ArrayList
class ConversationsListView() : BaseView(), OnQueryTextListener,
OnItemClickListener, OnItemLongClickListener {
lateinit var viewModel: ConversationsListViewModel
val factory: ConversationListViewModelFactory by inject()
private val recyclerViewAdapter = FlexibleAdapter(mutableListOf())
private var searchItem: MenuItem? = null
private var searchView: SearchView? = null
override fun onCreateOptionsMenu(
menu: Menu,
inflater: MenuInflater
) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.menu_conversation_plus_filter, menu)
searchItem = menu.findItem(R.id.action_search)
initSearchView()
}
override fun onPrepareOptionsMenu(menu: Menu) {
super.onPrepareOptionsMenu(menu)
if (recyclerViewAdapter.hasFilter()) {
searchItem?.expandActionView()
searchView?.setQuery(viewModel.searchQuery.value, false)
recyclerViewAdapter.filterItems()
}
loadUserAvatar(menu.findItem(R.id.action_settings))
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_settings -> {
val names = ArrayList<String>()
names.add("userAvatar.transitionTag")
router.pushController(
RouterTransaction.with(SettingsController())
.pushChangeHandler(
TransitionChangeHandlerCompat(
SharedElementTransition(names), VerticalChangeHandler()
)
)
.popChangeHandler(
TransitionChangeHandlerCompat(
SharedElementTransition(names), VerticalChangeHandler()
)
)
)
return true
}
else -> return super.onOptionsItemSelected(item)
}
}
private fun initSearchView() {
val searchManager = activity!!.getSystemService(Context.SEARCH_SERVICE) as SearchManager
searchView = MenuItemCompat.getActionView(searchItem) as SearchView
searchView!!.setMaxWidth(Integer.MAX_VALUE)
searchView!!.setInputType(InputType.TYPE_TEXT_VARIATION_FILTER)
var imeOptions = EditorInfo.IME_ACTION_DONE or EditorInfo.IME_FLAG_NO_FULLSCREEN
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && appPreferences.isKeyboardIncognito) {
imeOptions = imeOptions or EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING
}
searchView!!.setImeOptions(imeOptions)
searchView!!.setQueryHint(resources!!.getString(R.string.nc_search))
searchView!!.setSearchableInfo(searchManager.getSearchableInfo(activity!!.componentName))
searchView!!.setOnQueryTextListener(this)
}
override fun onQueryTextSubmit(query: String?): Boolean {
if (!viewModel.searchQuery.value.equals(query)) {
viewModel.searchQuery.value = query
}
return true
}
override fun onQueryTextChange(newText: String?): Boolean {
return onQueryTextSubmit(newText)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup
): View {
setHasOptionsMenu(true)
viewModel = viewModelProvider(factory).get(ConversationsListViewModel::class.java)
viewModel.apply {
viewState.observe(this@ConversationsListView, Observer { value ->
when (value) {
LOADING -> {
view?.recyclerView?.visibility = View.GONE
view?.emptyLayout?.visibility = View.GONE
view?.swipeRefreshLayoutView?.visibility = View.GONE
view?.progressBar?.visibility = View.VISIBLE
view?.floatingActionButton?.visibility = View.GONE
searchItem?.setVisible(false)
}
LOADED, FAILED -> {
view?.recyclerView?.visibility = View.VISIBLE
// The rest is handled in an actual network call
view?.progressBar?.visibility = View.GONE
view?.floatingActionButton?.visibility = View.VISIBLE
}
else -> {
// We should not be here
}
}
searchQuery.observe(this@ConversationsListView, Observer {
recyclerViewAdapter.setFilter(it)
recyclerViewAdapter.filterItems(500)
})
conversationsListData.observe(this@ConversationsListView, Observer {
val newConversations = mutableListOf<ConversationItem>()
for (conversation in it) {
newConversations.add(ConversationItem(conversation, viewModel.currentUser, activity))
}
recyclerViewAdapter.updateDataSet(newConversations as List<IFlexible<ViewHolder>>?)
if (it.isNotEmpty()) {
view?.emptyLayout?.visibility = View.GONE
view?.swipeRefreshLayoutView?.visibility = View.VISIBLE
searchItem?.setVisible(true)
} else {
view?.emptyLayout?.visibility = View.VISIBLE
view?.swipeRefreshLayoutView?.visibility = View.GONE
searchItem?.setVisible(false)
}
})
})
}
return super.onCreateView(inflater, container)
}
private fun loadUserAvatar(menuItem: MenuItem) {
if (activity != null) {
val avatarSize =
DisplayUtils.convertDpToPixel(menuItem.icon.intrinsicHeight.toFloat(), activity!!)
.toInt()
val imageRequest = DisplayUtils.getImageRequestForUrl(
ApiUtils.getUrlForAvatarWithNameAndPixels(
viewModel.currentUser.baseUrl,
viewModel.currentUser.userId, avatarSize
), null
)
val imagePipeline = Fresco.getImagePipeline()
val dataSource = imagePipeline.fetchDecodedImage(imageRequest, null)
dataSource.subscribe(object : BaseBitmapDataSubscriber() {
override fun onNewResultImpl(bitmap: Bitmap?) {
if (bitmap != null && resources != null) {
val roundedBitmapDrawable = RoundedBitmapDrawableFactory.create(resources!!, bitmap)
roundedBitmapDrawable.isCircular = true
roundedBitmapDrawable.setAntiAlias(true)
menuItem.icon = roundedBitmapDrawable
}
}
override fun onFailureImpl(dataSource: DataSource<CloseableReference<CloseableImage>>) {
menuItem.setIcon(R.drawable.ic_settings_white_24dp)
}
}, UiThreadImmediateExecutorService.getInstance())
}
}
override fun getLayoutId(): Int {
return R.layout.controller_conversations_rv
}
@OnClick(R.id.floatingActionButton, R.id.emptyLayout)
fun onFloatingActionButtonClick() {
val bundle = Bundle()
bundle.putBoolean(BundleKeys.KEY_NEW_CONVERSATION, true)
router.pushController(
RouterTransaction.with(ContactsController(bundle))
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler())
)
}
override fun getTitle(): String? {
return resources!!.getString(R.string.nc_app_name)
}
override fun onAttach(view: View) {
super.onAttach(view)
view.recyclerView.initRecyclerView(
SmoothScrollLinearLayoutManager(view.context), recyclerViewAdapter
)
recyclerViewAdapter.setFastScroller(view.fast_scroller)
recyclerViewAdapter.mItemClickListener = this
recyclerViewAdapter.mItemLongClickListener = this
view.fast_scroller.setBubbleTextCreator { position ->
var displayName =
(recyclerViewAdapter.getItem(position) as ConversationItem).model.displayName
if (displayName.length > 8) {
displayName = displayName.substring(0, 4) + "..."
}
displayName
}
viewModel.loadConversations()
}
override fun onItemLongClick(position: Int) {
val clickedItem = recyclerViewAdapter.getItem(position)
if (clickedItem != null) {
val conversation = (clickedItem as ConversationItem).model
val bundle = Bundle()
bundle.putParcelable(BundleKeys.KEY_ROOM, Parcels.wrap<Conversation>(conversation))
bundle.putParcelable(BundleKeys.KEY_MENU_TYPE, Parcels.wrap<MenuType>(REGULAR))
//prepareAndShowBottomSheetWithBundle(bundle, true)
}
}
override fun onItemClick(
view: View?,
position: Int
): Boolean {
val clickedItem = recyclerViewAdapter.getItem(position)
if (clickedItem != null) {
val conversation = (clickedItem as ConversationItem).model
val bundle = Bundle()
bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, viewModel.currentUser)
bundle.putString(BundleKeys.KEY_ROOM_TOKEN, conversation.token)
bundle.putString(BundleKeys.KEY_ROOM_ID, conversation.roomId)
bundle.putParcelable(BundleKeys.KEY_ACTIVE_CONVERSATION, Parcels.wrap(conversation))
ConductorRemapping.remapChatController(
router, viewModel.currentUser.getId(), conversation.token,
bundle, false
)
}
return true
}
}

View File

@ -0,0 +1,79 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* Copyright (C) 2017-2019 Mario Danic <mario@lovelyhq.com>
*
* This program 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.
*
* 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 com.nextcloud.talk.newarch.features.conversationsList
import androidx.lifecycle.MutableLiveData
import com.nextcloud.talk.models.database.UserEntity
import com.nextcloud.talk.models.json.conversations.Conversation
import com.nextcloud.talk.newarch.conversationsList.mvp.BaseViewModel
import com.nextcloud.talk.newarch.data.model.ErrorModel
import com.nextcloud.talk.newarch.domain.usecases.GetConversationsUseCase
import com.nextcloud.talk.newarch.domain.usecases.base.UseCaseResponse
import com.nextcloud.talk.newarch.mvvm.ViewState
import com.nextcloud.talk.newarch.mvvm.ViewState.FAILED
import com.nextcloud.talk.newarch.mvvm.ViewState.LOADED
import com.nextcloud.talk.newarch.mvvm.ViewState.LOADING
import com.nextcloud.talk.utils.database.user.UserUtils
import org.apache.commons.lang3.builder.CompareToBuilder
class ConversationsListViewModel constructor(
private val conversationsUseCase: GetConversationsUseCase,
private val userUtils: UserUtils
) : BaseViewModel<ConversationsListView>() {
val conversationsListData = MutableLiveData<List<Conversation>>()
val viewState = MutableLiveData<ViewState>(LOADING)
val messageData = MutableLiveData<String>()
val searchQuery = MutableLiveData<String>()
lateinit var currentUser: UserEntity
fun loadConversations() {
currentUser = userUtils.currentUser
if (!conversationsUseCase.isUserInitialized() || conversationsUseCase.user != currentUser) {
conversationsUseCase.user = currentUser
viewState.value = LOADING
}
conversationsUseCase.invoke(
backgroundAndUIScope, null, object : UseCaseResponse<List<Conversation>> {
override fun onSuccess(result: List<Conversation>) {
val newConversations = result.toMutableList()
newConversations.sortWith(Comparator { conversation1, conversation2 ->
CompareToBuilder()
.append(conversation2.isFavorite, conversation1.isFavorite)
.append(conversation2.lastActivity, conversation1.lastActivity)
.toComparison()
})
conversationsListData.value = newConversations
viewState.value = LOADED
}
override fun onError(errorModel: ErrorModel?) {
messageData.value = errorModel?.message
viewState.value = FAILED
}
})
}
}

View File

@ -0,0 +1,47 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* Copyright (C) 2017-2019 Mario Danic <mario@lovelyhq.com>
*
* This program 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.
*
* 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 com.nextcloud.talk.newarch.features.conversationsList.di.module
import com.nextcloud.talk.newarch.data.source.remote.ApiErrorHandler
import com.nextcloud.talk.newarch.di.module.createApiErrorHandler
import com.nextcloud.talk.newarch.domain.repository.NextcloudTalkRepository
import com.nextcloud.talk.newarch.domain.usecases.GetConversationsUseCase
import com.nextcloud.talk.newarch.features.conversationsList.ConversationListViewModelFactory
import com.nextcloud.talk.utils.database.user.UserUtils
import org.koin.dsl.module
val ConversationsListModule = module {
single { createGetConversationsUseCase(get(), createApiErrorHandler()) }
//viewModel { ConversationsListViewModel(get(), get()) }
factory { createConversationListViewModelFactory(get(), get()) }
}
fun createGetConversationsUseCase(
nextcloudTalkRepository: NextcloudTalkRepository,
apiErrorHandler: ApiErrorHandler
): GetConversationsUseCase {
return GetConversationsUseCase(nextcloudTalkRepository, apiErrorHandler)
}
fun createConversationListViewModelFactory(conversationsUseCase: GetConversationsUseCase,
userUtils: UserUtils): ConversationListViewModelFactory {
return ConversationListViewModelFactory(conversationsUseCase, userUtils)
}

View File

@ -0,0 +1,57 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* Copyright (C) 2017-2019 Mario Danic <mario@lovelyhq.com>
*
* This program 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.
*
* 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/>.
*
* Heavily inspired by https://android.jlelse.eu/implementing-search-on-type-in-android-with-coroutines-ab117c8f13a4
*/
package com.nextcloud.talk.newarch.features.search
import android.widget.SearchView
import androidx.appcompat.widget.SearchView.OnQueryTextListener
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class DebouncingQueryTextListener(
lifecycle: Lifecycle,
private val onDebouncingQueryTextChange: (String?) -> Unit
) : OnQueryTextListener {
var debouncePeriod: Long = 500
private val coroutineScope = lifecycle.coroutineScope
private var searchJob: Job? = null
override fun onQueryTextSubmit(query: String?): Boolean {
return false
}
override fun onQueryTextChange(newText: String?): Boolean {
searchJob?.cancel()
searchJob = coroutineScope.launch {
newText?.let {
delay(debouncePeriod)
onDebouncingQueryTextChange(newText)
}
}
return false
}
}

View File

@ -0,0 +1,83 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* Copyright (C) 2017-2019 Mario Danic <mario@lovelyhq.com>
*
* This program 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.
*
* 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 com.nextcloud.talk.newarch.conversationsList.mvp
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.LayoutRes
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import butterknife.ButterKnife
import com.bluelinelabs.conductor.archlifecycle.ControllerLifecycleOwner
import com.nextcloud.talk.controllers.base.BaseController
import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.ViewModelProvider
import kotlinx.android.extensions.LayoutContainer
abstract class BaseView : BaseController(), LifecycleOwner, ViewModelStoreOwner {
private val viewModelStore = ViewModelStore()
private val lifecycleOwner = ControllerLifecycleOwner(this)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup): View {
val view = inflater.inflate(getLayoutId(), container, false)
unbinder = ButterKnife.bind(this, view)
onViewBound(view)
return view
}
override fun inflateView(
inflater: LayoutInflater,
container: ViewGroup
): View {
return inflateView(inflater, container)
}
override fun onDestroy() {
super.onDestroy()
viewModelStore.clear();
}
override fun getLifecycle(): Lifecycle {
return lifecycleOwner.lifecycle
}
fun viewModelProvider(): ViewModelProvider {
return viewModelProvider(ViewModelProvider.AndroidViewModelFactory(activity!!.application))
}
fun viewModelProvider(factory: ViewModelProvider.NewInstanceFactory): ViewModelProvider {
return ViewModelProvider(viewModelStore, factory)
}
fun viewModelProvider(factory: ViewModelProvider.Factory): ViewModelProvider {
return ViewModelProvider(viewModelStore, factory)
}
override fun getViewModelStore(): ViewModelStore {
return viewModelStore
}
@LayoutRes
protected abstract fun getLayoutId(): Int
}

View File

@ -20,23 +20,29 @@
package com.nextcloud.talk.newarch.conversationsList.mvp package com.nextcloud.talk.newarch.conversationsList.mvp
import androidx.lifecycle.ViewModel
import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.CompositeDisposable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
abstract class BasePresenter<V : BaseView> : MvpPresenter { abstract class BaseViewModel<V : BaseView> : ViewModel() {
protected val disposables: CompositeDisposable = CompositeDisposable() protected val disposables: CompositeDisposable = CompositeDisposable()
protected var view: V? = null
private set
fun start(view: V) { val backgroundAndUIScope = CoroutineScope(
this.view = view Job() + Dispatchers.Main
} )
override fun stop() { val backgroundScope = CoroutineScope(
this.view = null Job()
} )
override fun destroy() { override fun onCleared() {
super.onCleared()
disposables.clear() disposables.clear()
backgroundAndUIScope.cancel()
backgroundScope.cancel()
} }
} }

View File

@ -18,9 +18,10 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package com.nextcloud.talk.newarch.conversationsList.mvp package com.nextcloud.talk.newarch.mvvm
interface MvpPresenter { enum class ViewState {
fun stop() LOADING,
fun destroy() LOADED,
FAILED
} }

View File

@ -18,17 +18,16 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package com.nextcloud.talk.newarch.conversationsList package com.nextcloud.talk.newarch.mvvm.ext
import com.nextcloud.talk.models.json.conversations.Conversation import androidx.recyclerview.widget.RecyclerView
class ConversationsListContract { fun RecyclerView.initRecyclerView(
interface View { layoutManager: RecyclerView.LayoutManager,
fun onLoadConversationsSuccess(conversations: List<Conversation>) adapter: RecyclerView.Adapter<out RecyclerView.ViewHolder>,
fun onLoadConversationsFailure(throwable: Throwable) hasFixedSize: Boolean = true
} ) {
this.layoutManager = layoutManager
interface Presenter { this.adapter = adapter
fun loadConversations() setHasFixedSize(hasFixedSize)
}
} }

View File

@ -27,7 +27,13 @@ import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
import com.nextcloud.talk.controllers.ChatController import com.nextcloud.talk.controllers.ChatController
object ConductorRemapping { object ConductorRemapping {
fun remapChatController(router: Router, internalUserId: Long, roomTokenOrId: String, bundle: Bundle, replaceTop: Boolean) { fun remapChatController(
router: Router,
internalUserId: Long,
roomTokenOrId: String,
bundle: Bundle,
replaceTop: Boolean
) {
val tag = "$internalUserId@$roomTokenOrId" val tag = "$internalUserId@$roomTokenOrId"
if (router.getControllerWithTag(tag) != null) { if (router.getControllerWithTag(tag) != null) {
val backstack = router.backstack val backstack = router.backstack
@ -44,13 +50,17 @@ object ConductorRemapping {
router.setBackstack(backstack, HorizontalChangeHandler()) router.setBackstack(backstack, HorizontalChangeHandler())
} else { } else {
if (!replaceTop) { if (!replaceTop) {
router.pushController(RouterTransaction.with(ChatController(bundle)) router.pushController(
RouterTransaction.with(ChatController(bundle))
.pushChangeHandler(HorizontalChangeHandler()) .pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler()).tag(tag)) .popChangeHandler(HorizontalChangeHandler()).tag(tag)
)
} else { } else {
router.replaceTopController(RouterTransaction.with(ChatController(bundle)) router.replaceTopController(
RouterTransaction.with(ChatController(bundle))
.pushChangeHandler(HorizontalChangeHandler()) .pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler()).tag(tag)) .popChangeHandler(HorizontalChangeHandler()).tag(tag)
)
} }
} }
} }

View File

@ -128,7 +128,7 @@ public class PushUtils {
} catch (NoSuchAlgorithmException e) { } catch (NoSuchAlgorithmException e) {
Log.d(TAG, "No such algorithm"); Log.d(TAG, "No such algorithm");
} catch (IOException e) { } catch (IOException e) {
Log.d(TAG, "Error while trying to parse push configuration state"); Log.d(TAG, "Error while trying to parse push configuration viewState");
} catch (InvalidKeyException e) { } catch (InvalidKeyException e) {
Log.d(TAG, "Invalid key while trying to verify"); Log.d(TAG, "Invalid key while trying to verify");
} catch (SignatureException e) { } catch (SignatureException e) {

View File

@ -125,7 +125,7 @@ public class MagicAudioManager {
// Tablet devices (e.g. Nexus 7) does not support proximity sensors. // Tablet devices (e.g. Nexus 7) does not support proximity sensors.
// Note that, the sensor will not be active until start() has been called. // Note that, the sensor will not be active until start() has been called.
proximitySensor = MagicProximitySensor.create(context, new Runnable() { proximitySensor = MagicProximitySensor.create(context, new Runnable() {
// This method will be called each time a state change is detected. // This method will be called each time a viewState change is detected.
// Example: user holds his hand over the device (closer than ~5 cm), // Example: user holds his hand over the device (closer than ~5 cm),
// or removes his hand from the device. // or removes his hand from the device.
public void run() { public void run() {
@ -160,7 +160,7 @@ public class MagicAudioManager {
} }
/** /**
* This method is called when the proximity sensor reports a state change, * This method is called when the proximity sensor reports a viewState change,
* e.g. from "NEAR to FAR" or from "FAR to NEAR". * e.g. from "NEAR to FAR" or from "FAR to NEAR".
*/ */
private void onProximitySensorChangedState() { private void onProximitySensorChangedState() {
@ -205,7 +205,7 @@ public class MagicAudioManager {
this.audioManagerEvents = audioManagerEvents; this.audioManagerEvents = audioManagerEvents;
amState = AudioManagerState.RUNNING; amState = AudioManagerState.RUNNING;
// Store current audio state so we can restore it when stop() is called. // Store current audio viewState so we can restore it when stop() is called.
savedAudioMode = audioManager.getMode(); savedAudioMode = audioManager.getMode();
savedIsSpeakerPhoneOn = audioManager.isSpeakerphoneOn(); savedIsSpeakerPhoneOn = audioManager.isSpeakerphoneOn();
savedIsMicrophoneMute = audioManager.isMicrophoneMute(); savedIsMicrophoneMute = audioManager.isMicrophoneMute();
@ -294,7 +294,7 @@ public class MagicAudioManager {
Log.d(TAG, "stop"); Log.d(TAG, "stop");
ThreadUtils.checkIsOnMainThread(); ThreadUtils.checkIsOnMainThread();
if (amState != AudioManagerState.RUNNING) { if (amState != AudioManagerState.RUNNING) {
Log.e(TAG, "Trying to stop AudioManager in incorrect state: " + amState); Log.e(TAG, "Trying to stop AudioManager in incorrect viewState: " + amState);
return; return;
} }
amState = AudioManagerState.UNINITIALIZED; amState = AudioManagerState.UNINITIALIZED;
@ -434,7 +434,7 @@ public class MagicAudioManager {
} }
/** /**
* Sets the microphone mute state. * Sets the microphone mute viewState.
*/ */
private void setMicrophoneMute(boolean on) { private void setMicrophoneMute(boolean on) {
boolean wasMuted = audioManager.isMicrophoneMute(); boolean wasMuted = audioManager.isMicrophoneMute();
@ -445,7 +445,7 @@ public class MagicAudioManager {
} }
/** /**
* Gets the current earpiece state. * Gets the current earpiece viewState.
*/ */
private boolean hasEarpiece() { private boolean hasEarpiece() {
return magicContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY); return magicContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY);
@ -480,21 +480,21 @@ public class MagicAudioManager {
/** /**
* Updates list of possible audio devices and make new device selection. * Updates list of possible audio devices and make new device selection.
* TODO(henrika): add unit test to verify all state transitions. * TODO(henrika): add unit test to verify all viewState transitions.
*/ */
public void updateAudioDeviceState() { public void updateAudioDeviceState() {
ThreadUtils.checkIsOnMainThread(); ThreadUtils.checkIsOnMainThread();
Log.d(TAG, "--- updateAudioDeviceState: " Log.d(TAG, "--- updateAudioDeviceState: "
+ "wired headset=" + hasWiredHeadset + ", " + "wired headset=" + hasWiredHeadset + ", "
+ "BT state=" + bluetoothManager.getState()); + "BT viewState=" + bluetoothManager.getState());
Log.d(TAG, "Device status: " Log.d(TAG, "Device status: "
+ "available=" + audioDevices + ", " + "available=" + audioDevices + ", "
+ "selected=" + selectedAudioDevice + ", " + "selected=" + selectedAudioDevice + ", "
+ "user selected=" + userSelectedAudioDevice); + "user selected=" + userSelectedAudioDevice);
// Check if any Bluetooth headset is connected. The internal BT state will // Check if any Bluetooth headset is connected. The internal BT viewState will
// change accordingly. // change accordingly.
// TODO(henrika): perhaps wrap required state into BT manager. // TODO(henrika): perhaps wrap required viewState into BT manager.
if (bluetoothManager.getState() == MagicBluetoothManager.State.HEADSET_AVAILABLE if (bluetoothManager.getState() == MagicBluetoothManager.State.HEADSET_AVAILABLE
|| bluetoothManager.getState() == MagicBluetoothManager.State.HEADSET_UNAVAILABLE || bluetoothManager.getState() == MagicBluetoothManager.State.HEADSET_UNAVAILABLE
|| bluetoothManager.getState() == MagicBluetoothManager.State.SCO_DISCONNECTING) { || bluetoothManager.getState() == MagicBluetoothManager.State.SCO_DISCONNECTING) {
@ -521,7 +521,7 @@ public class MagicAudioManager {
newAudioDevices.add(AudioDevice.EARPIECE); newAudioDevices.add(AudioDevice.EARPIECE);
} }
} }
// Store state which is set to true if the device list has changed. // Store viewState which is set to true if the device list has changed.
boolean audioDeviceSetUpdated = !audioDevices.equals(newAudioDevices); boolean audioDeviceSetUpdated = !audioDevices.equals(newAudioDevices);
// Update the existing audio device set. // Update the existing audio device set.
audioDevices = newAudioDevices; audioDevices = newAudioDevices;
@ -562,7 +562,7 @@ public class MagicAudioManager {
|| bluetoothManager.getState() == MagicBluetoothManager.State.SCO_CONNECTED) { || bluetoothManager.getState() == MagicBluetoothManager.State.SCO_CONNECTED) {
Log.d(TAG, "Need BT audio: start=" + needBluetoothAudioStart + ", " Log.d(TAG, "Need BT audio: start=" + needBluetoothAudioStart + ", "
+ "stop=" + needBluetoothAudioStop + ", " + "stop=" + needBluetoothAudioStop + ", "
+ "BT state=" + bluetoothManager.getState()); + "BT viewState=" + bluetoothManager.getState());
} }
// Start or stop Bluetooth SCO connection given states set earlier. // Start or stop Bluetooth SCO connection given states set earlier.
@ -624,7 +624,7 @@ public class MagicAudioManager {
} }
/** /**
* AudioManager state. * AudioManager viewState.
*/ */
public enum AudioManagerState { public enum AudioManagerState {
UNINITIALIZED, UNINITIALIZED,
@ -649,7 +649,7 @@ public class MagicAudioManager {
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
int state = intent.getIntExtra("state", STATE_UNPLUGGED); int state = intent.getIntExtra("viewState", STATE_UNPLUGGED);
// int microphone = intent.getIntExtra("microphone", HAS_NO_MIC); // int microphone = intent.getIntExtra("microphone", HAS_NO_MIC);
// String name = intent.getStringExtra("name"); // String name = intent.getStringExtra("name");
hasWiredHeadset = (state == STATE_PLUGGED); hasWiredHeadset = (state == STATE_PLUGGED);

View File

@ -98,7 +98,7 @@ public class MagicBluetoothManager {
} }
/** /**
* Returns the internal state. * Returns the internal viewState.
*/ */
public State getState() { public State getState() {
ThreadUtils.checkIsOnMainThread(); ThreadUtils.checkIsOnMainThread();
@ -110,14 +110,14 @@ public class MagicBluetoothManager {
/** /**
* Activates components required to detect Bluetooth devices and to enable * Activates components required to detect Bluetooth devices and to enable
* BT SCO (audio is routed via BT SCO) for the headset profile. The end * BT SCO (audio is routed via BT SCO) for the headset profile. The end
* state will be HEADSET_UNAVAILABLE but a state machine has started which * viewState will be HEADSET_UNAVAILABLE but a viewState machine has started which
* will start a state change sequence where the final outcome depends on * will start a viewState change sequence where the final outcome depends on
* if/when the BT headset is enabled. * if/when the BT headset is enabled.
* Example of state change sequence when start() is called while BT device * Example of viewState change sequence when start() is called while BT device
* is connected and enabled: * is connected and enabled:
* UNINITIALIZED --> HEADSET_UNAVAILABLE --> HEADSET_AVAILABLE --> * UNINITIALIZED --> HEADSET_UNAVAILABLE --> HEADSET_AVAILABLE -->
* SCO_CONNECTING --> SCO_CONNECTED <==> audio is now routed via BT SCO. * SCO_CONNECTING --> SCO_CONNECTED <==> audio is now routed via BT SCO.
* Note that the MagicAudioManager is also involved in driving this state * Note that the MagicAudioManager is also involved in driving this viewState
* change. * change.
*/ */
public void start() { public void start() {
@ -128,7 +128,7 @@ public class MagicBluetoothManager {
return; return;
} }
if (bluetoothState != State.UNINITIALIZED) { if (bluetoothState != State.UNINITIALIZED) {
Log.w(TAG, "Invalid BT state"); Log.w(TAG, "Invalid BT viewState");
return; return;
} }
bluetoothHeadset = null; bluetoothHeadset = null;
@ -155,16 +155,16 @@ public class MagicBluetoothManager {
} }
// Register receivers for BluetoothHeadset change notifications. // Register receivers for BluetoothHeadset change notifications.
IntentFilter bluetoothHeadsetFilter = new IntentFilter(); IntentFilter bluetoothHeadsetFilter = new IntentFilter();
// Register receiver for change in connection state of the Headset profile. // Register receiver for change in connection viewState of the Headset profile.
bluetoothHeadsetFilter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED); bluetoothHeadsetFilter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
// Register receiver for change in audio connection state of the Headset profile. // Register receiver for change in audio connection viewState of the Headset profile.
bluetoothHeadsetFilter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED); bluetoothHeadsetFilter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);
registerReceiver(bluetoothHeadsetReceiver, bluetoothHeadsetFilter); registerReceiver(bluetoothHeadsetReceiver, bluetoothHeadsetFilter);
Log.d(TAG, "HEADSET profile state: " Log.d(TAG, "HEADSET profile viewState: "
+ stateToString(bluetoothAdapter.getProfileConnectionState(BluetoothProfile.HEADSET))); + stateToString(bluetoothAdapter.getProfileConnectionState(BluetoothProfile.HEADSET)));
Log.d(TAG, "Bluetooth proxy for headset profile has started"); Log.d(TAG, "Bluetooth proxy for headset profile has started");
bluetoothState = State.HEADSET_UNAVAILABLE; bluetoothState = State.HEADSET_UNAVAILABLE;
Log.d(TAG, "start done: BT state=" + bluetoothState); Log.d(TAG, "start done: BT viewState=" + bluetoothState);
} }
/** /**
@ -172,7 +172,7 @@ public class MagicBluetoothManager {
*/ */
public void stop() { public void stop() {
ThreadUtils.checkIsOnMainThread(); ThreadUtils.checkIsOnMainThread();
Log.d(TAG, "stop: BT state=" + bluetoothState); Log.d(TAG, "stop: BT viewState=" + bluetoothState);
if (bluetoothAdapter == null) { if (bluetoothAdapter == null) {
return; return;
} }
@ -191,7 +191,7 @@ public class MagicBluetoothManager {
bluetoothAdapter = null; bluetoothAdapter = null;
bluetoothDevice = null; bluetoothDevice = null;
bluetoothState = State.UNINITIALIZED; bluetoothState = State.UNINITIALIZED;
Log.d(TAG, "stop done: BT state=" + bluetoothState); Log.d(TAG, "stop done: BT viewState=" + bluetoothState);
} }
/** /**
@ -209,7 +209,7 @@ public class MagicBluetoothManager {
*/ */
public boolean startScoAudio() { public boolean startScoAudio() {
ThreadUtils.checkIsOnMainThread(); ThreadUtils.checkIsOnMainThread();
Log.d(TAG, "startSco: BT state=" + bluetoothState + ", " Log.d(TAG, "startSco: BT viewState=" + bluetoothState + ", "
+ "attempts: " + scoConnectionAttempts + ", " + "attempts: " + scoConnectionAttempts + ", "
+ "SCO is on: " + isScoOn()); + "SCO is on: " + isScoOn());
if (scoConnectionAttempts >= MAX_SCO_CONNECTION_ATTEMPTS) { if (scoConnectionAttempts >= MAX_SCO_CONNECTION_ATTEMPTS) {
@ -224,13 +224,13 @@ public class MagicBluetoothManager {
Log.d(TAG, "Starting Bluetooth SCO and waits for ACTION_AUDIO_STATE_CHANGED..."); Log.d(TAG, "Starting Bluetooth SCO and waits for ACTION_AUDIO_STATE_CHANGED...");
// The SCO connection establishment can take several seconds, hence we cannot rely on the // The SCO connection establishment can take several seconds, hence we cannot rely on the
// connection to be available when the method returns but instead register to receive the // connection to be available when the method returns but instead register to receive the
// intent ACTION_SCO_AUDIO_STATE_UPDATED and wait for the state to be SCO_AUDIO_STATE_CONNECTED. // intent ACTION_SCO_AUDIO_STATE_UPDATED and wait for the viewState to be SCO_AUDIO_STATE_CONNECTED.
bluetoothState = State.SCO_CONNECTING; bluetoothState = State.SCO_CONNECTING;
audioManager.startBluetoothSco(); audioManager.startBluetoothSco();
audioManager.setBluetoothScoOn(true); audioManager.setBluetoothScoOn(true);
scoConnectionAttempts++; scoConnectionAttempts++;
startTimer(); startTimer();
Log.d(TAG, "startScoAudio done: BT state=" + bluetoothState + ", " Log.d(TAG, "startScoAudio done: BT viewState=" + bluetoothState + ", "
+ "SCO is on: " + isScoOn()); + "SCO is on: " + isScoOn());
return true; return true;
} }
@ -240,7 +240,7 @@ public class MagicBluetoothManager {
*/ */
public void stopScoAudio() { public void stopScoAudio() {
ThreadUtils.checkIsOnMainThread(); ThreadUtils.checkIsOnMainThread();
Log.d(TAG, "stopScoAudio: BT state=" + bluetoothState + ", " Log.d(TAG, "stopScoAudio: BT viewState=" + bluetoothState + ", "
+ "SCO is on: " + isScoOn()); + "SCO is on: " + isScoOn());
if (bluetoothState != State.SCO_CONNECTING && bluetoothState != State.SCO_CONNECTED) { if (bluetoothState != State.SCO_CONNECTING && bluetoothState != State.SCO_CONNECTED) {
return; return;
@ -249,14 +249,14 @@ public class MagicBluetoothManager {
audioManager.stopBluetoothSco(); audioManager.stopBluetoothSco();
audioManager.setBluetoothScoOn(false); audioManager.setBluetoothScoOn(false);
bluetoothState = State.SCO_DISCONNECTING; bluetoothState = State.SCO_DISCONNECTING;
Log.d(TAG, "stopScoAudio done: BT state=" + bluetoothState + ", " Log.d(TAG, "stopScoAudio done: BT viewState=" + bluetoothState + ", "
+ "SCO is on: " + isScoOn()); + "SCO is on: " + isScoOn());
} }
/** /**
* Use the BluetoothHeadset proxy object (controls the Bluetooth Headset * Use the BluetoothHeadset proxy object (controls the Bluetooth Headset
* Service via IPC) to update the list of connected devices for the HEADSET * Service via IPC) to update the list of connected devices for the HEADSET
* profile. The internal state will change to HEADSET_UNAVAILABLE or to * profile. The internal viewState will change to HEADSET_UNAVAILABLE or to
* HEADSET_AVAILABLE and |bluetoothDevice| will be mapped to the connected * HEADSET_AVAILABLE and |bluetoothDevice| will be mapped to the connected
* device if available. * device if available.
*/ */
@ -266,7 +266,7 @@ public class MagicBluetoothManager {
} }
Log.d(TAG, "updateDevice"); Log.d(TAG, "updateDevice");
// Get connected devices for the headset profile. Returns the set of // Get connected devices for the headset profile. Returns the set of
// devices which are in state STATE_CONNECTED. The BluetoothDevice class // devices which are in viewState STATE_CONNECTED. The BluetoothDevice class
// is just a thin wrapper for a Bluetooth hardware address. // is just a thin wrapper for a Bluetooth hardware address.
List<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices(); List<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices();
if (devices.isEmpty()) { if (devices.isEmpty()) {
@ -279,10 +279,10 @@ public class MagicBluetoothManager {
bluetoothState = State.HEADSET_AVAILABLE; bluetoothState = State.HEADSET_AVAILABLE;
Log.d(TAG, "Connected bluetooth headset: " Log.d(TAG, "Connected bluetooth headset: "
+ "name=" + bluetoothDevice.getName() + ", " + "name=" + bluetoothDevice.getName() + ", "
+ "state=" + stateToString(bluetoothHeadset.getConnectionState(bluetoothDevice)) + "viewState=" + stateToString(bluetoothHeadset.getConnectionState(bluetoothDevice))
+ ", SCO audio=" + bluetoothHeadset.isAudioConnected(bluetoothDevice)); + ", SCO audio=" + bluetoothHeadset.isAudioConnected(bluetoothDevice));
} }
Log.d(TAG, "updateDevice done: BT state=" + bluetoothState); Log.d(TAG, "updateDevice done: BT viewState=" + bluetoothState);
} }
/** /**
@ -311,13 +311,13 @@ public class MagicBluetoothManager {
} }
/** /**
* Logs the state of the local Bluetooth adapter. * Logs the viewState of the local Bluetooth adapter.
*/ */
@SuppressLint("HardwareIds") @SuppressLint("HardwareIds")
protected void logBluetoothAdapterInfo(BluetoothAdapter localAdapter) { protected void logBluetoothAdapterInfo(BluetoothAdapter localAdapter) {
Log.d(TAG, "BluetoothAdapter: " Log.d(TAG, "BluetoothAdapter: "
+ "enabled=" + localAdapter.isEnabled() + ", " + "enabled=" + localAdapter.isEnabled() + ", "
+ "state=" + stateToString(localAdapter.getState()) + ", " + "viewState=" + stateToString(localAdapter.getState()) + ", "
+ "name=" + localAdapter.getName() + ", " + "name=" + localAdapter.getName() + ", "
+ "address=" + localAdapter.getAddress()); + "address=" + localAdapter.getAddress());
// Log the set of BluetoothDevice objects that are bonded (paired) to the local adapter. // Log the set of BluetoothDevice objects that are bonded (paired) to the local adapter.
@ -366,7 +366,7 @@ public class MagicBluetoothManager {
if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) { if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) {
return; return;
} }
Log.d(TAG, "bluetoothTimeout: BT state=" + bluetoothState + ", " Log.d(TAG, "bluetoothTimeout: BT viewState=" + bluetoothState + ", "
+ "attempts: " + scoConnectionAttempts + ", " + "attempts: " + scoConnectionAttempts + ", "
+ "SCO is on: " + isScoOn()); + "SCO is on: " + isScoOn());
if (bluetoothState != State.SCO_CONNECTING) { if (bluetoothState != State.SCO_CONNECTING) {
@ -385,7 +385,7 @@ public class MagicBluetoothManager {
} }
} }
if (scoConnected) { if (scoConnected) {
// We thought BT had timed out, but it's actually on; updating state. // We thought BT had timed out, but it's actually on; updating viewState.
bluetoothState = State.SCO_CONNECTED; bluetoothState = State.SCO_CONNECTED;
scoConnectionAttempts = 0; scoConnectionAttempts = 0;
} else { } else {
@ -394,7 +394,7 @@ public class MagicBluetoothManager {
stopScoAudio(); stopScoAudio();
} }
updateAudioDeviceState(); updateAudioDeviceState();
Log.d(TAG, "bluetoothTimeout done: BT state=" + bluetoothState); Log.d(TAG, "bluetoothTimeout done: BT viewState=" + bluetoothState);
} }
/** /**
@ -434,7 +434,7 @@ public class MagicBluetoothManager {
} }
} }
// Bluetooth connection state. // Bluetooth connection viewState.
public enum State { public enum State {
// Bluetooth is not available; no adapter or Bluetooth is off. // Bluetooth is not available; no adapter or Bluetooth is off.
UNINITIALIZED, UNINITIALIZED,
@ -461,17 +461,17 @@ public class MagicBluetoothManager {
private class BluetoothServiceListener implements BluetoothProfile.ServiceListener { private class BluetoothServiceListener implements BluetoothProfile.ServiceListener {
@Override @Override
// Called to notify the client when the proxy object has been connected to the service. // Called to notify the client when the proxy object has been connected to the service.
// Once we have the profile proxy object, we can use it to monitor the state of the // Once we have the profile proxy object, we can use it to monitor the viewState of the
// connection and perform other operations that are relevant to the headset profile. // connection and perform other operations that are relevant to the headset profile.
public void onServiceConnected(int profile, BluetoothProfile proxy) { public void onServiceConnected(int profile, BluetoothProfile proxy) {
if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) { if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) {
return; return;
} }
Log.d(TAG, "BluetoothServiceListener.onServiceConnected: BT state=" + bluetoothState); Log.d(TAG, "BluetoothServiceListener.onServiceConnected: BT viewState=" + bluetoothState);
// Android only supports one connected Bluetooth Headset at a time. // Android only supports one connected Bluetooth Headset at a time.
bluetoothHeadset = (BluetoothHeadset) proxy; bluetoothHeadset = (BluetoothHeadset) proxy;
updateAudioDeviceState(); updateAudioDeviceState();
Log.d(TAG, "onServiceConnected done: BT state=" + bluetoothState); Log.d(TAG, "onServiceConnected done: BT viewState=" + bluetoothState);
} }
@Override @Override
@ -480,18 +480,18 @@ public class MagicBluetoothManager {
if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) { if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) {
return; return;
} }
Log.d(TAG, "BluetoothServiceListener.onServiceDisconnected: BT state=" + bluetoothState); Log.d(TAG, "BluetoothServiceListener.onServiceDisconnected: BT viewState=" + bluetoothState);
stopScoAudio(); stopScoAudio();
bluetoothHeadset = null; bluetoothHeadset = null;
bluetoothDevice = null; bluetoothDevice = null;
bluetoothState = State.HEADSET_UNAVAILABLE; bluetoothState = State.HEADSET_UNAVAILABLE;
updateAudioDeviceState(); updateAudioDeviceState();
Log.d(TAG, "onServiceDisconnected done: BT state=" + bluetoothState); Log.d(TAG, "onServiceDisconnected done: BT viewState=" + bluetoothState);
} }
} }
// Intent broadcast receiver which handles changes in Bluetooth device availability. // Intent broadcast receiver which handles changes in Bluetooth device availability.
// Detects headset changes and Bluetooth SCO state changes. // Detects headset changes and Bluetooth SCO viewState changes.
private class BluetoothHeadsetBroadcastReceiver extends BroadcastReceiver { private class BluetoothHeadsetBroadcastReceiver extends BroadcastReceiver {
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
@ -499,7 +499,7 @@ public class MagicBluetoothManager {
return; return;
} }
final String action = intent.getAction(); final String action = intent.getAction();
// Change in connection state of the Headset profile. Note that the // Change in connection viewState of the Headset profile. Note that the
// change does not tell us anything about whether we're streaming // change does not tell us anything about whether we're streaming
// audio to BT over SCO. Typically received when user turns on a BT // audio to BT over SCO. Typically received when user turns on a BT
// headset while audio is active using another audio device. // headset while audio is active using another audio device.
@ -510,7 +510,7 @@ public class MagicBluetoothManager {
+ "a=ACTION_CONNECTION_STATE_CHANGED, " + "a=ACTION_CONNECTION_STATE_CHANGED, "
+ "s=" + stateToString(state) + ", " + "s=" + stateToString(state) + ", "
+ "sb=" + isInitialStickyBroadcast() + ", " + "sb=" + isInitialStickyBroadcast() + ", "
+ "BT state: " + bluetoothState); + "BT viewState: " + bluetoothState);
if (state == BluetoothHeadset.STATE_CONNECTED) { if (state == BluetoothHeadset.STATE_CONNECTED) {
scoConnectionAttempts = 0; scoConnectionAttempts = 0;
updateAudioDeviceState(); updateAudioDeviceState();
@ -523,7 +523,7 @@ public class MagicBluetoothManager {
stopScoAudio(); stopScoAudio();
updateAudioDeviceState(); updateAudioDeviceState();
} }
// Change in the audio (SCO) connection state of the Headset profile. // Change in the audio (SCO) connection viewState of the Headset profile.
// Typically received after call to startScoAudio() has finalized. // Typically received after call to startScoAudio() has finalized.
} else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) { } else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) {
final int state = intent.getIntExtra( final int state = intent.getIntExtra(
@ -532,7 +532,7 @@ public class MagicBluetoothManager {
+ "a=ACTION_AUDIO_STATE_CHANGED, " + "a=ACTION_AUDIO_STATE_CHANGED, "
+ "s=" + stateToString(state) + ", " + "s=" + stateToString(state) + ", "
+ "sb=" + isInitialStickyBroadcast() + ", " + "sb=" + isInitialStickyBroadcast() + ", "
+ "BT state: " + bluetoothState); + "BT viewState: " + bluetoothState);
if (state == BluetoothHeadset.STATE_AUDIO_CONNECTED) { if (state == BluetoothHeadset.STATE_AUDIO_CONNECTED) {
cancelTimer(); cancelTimer();
if (bluetoothState == State.SCO_CONNECTING) { if (bluetoothState == State.SCO_CONNECTING) {
@ -541,7 +541,7 @@ public class MagicBluetoothManager {
scoConnectionAttempts = 0; scoConnectionAttempts = 0;
updateAudioDeviceState(); updateAudioDeviceState();
} else { } else {
Log.w(TAG, "Unexpected state BluetoothHeadset.STATE_AUDIO_CONNECTED"); Log.w(TAG, "Unexpected viewState BluetoothHeadset.STATE_AUDIO_CONNECTED");
} }
} else if (state == BluetoothHeadset.STATE_AUDIO_CONNECTING) { } else if (state == BluetoothHeadset.STATE_AUDIO_CONNECTING) {
Log.d(TAG, "+++ Bluetooth audio SCO is now connecting..."); Log.d(TAG, "+++ Bluetooth audio SCO is now connecting...");
@ -554,7 +554,7 @@ public class MagicBluetoothManager {
updateAudioDeviceState(); updateAudioDeviceState();
} }
} }
Log.d(TAG, "onReceive done: BT state=" + bluetoothState); Log.d(TAG, "onReceive done: BT viewState=" + bluetoothState);
} }
} }
} }

View File

@ -100,7 +100,7 @@ public class MagicProximitySensor implements SensorEventListener {
} }
/** /**
* Getter for last reported state. Set to true if "near" is reported. * Getter for last reported viewState. Set to true if "near" is reported.
*/ */
boolean sensorReportsNearState() { boolean sensorReportsNearState() {
threadChecker.checkIsOnValidThread(); threadChecker.checkIsOnValidThread();
@ -124,15 +124,15 @@ public class MagicProximitySensor implements SensorEventListener {
// avoid blocking. // avoid blocking.
float distanceInCentimeters = event.values[0]; float distanceInCentimeters = event.values[0];
if (distanceInCentimeters < proximitySensor.getMaximumRange()) { if (distanceInCentimeters < proximitySensor.getMaximumRange()) {
Log.d(TAG, "Proximity sensor => NEAR state"); Log.d(TAG, "Proximity sensor => NEAR viewState");
lastStateReportIsNear = true; lastStateReportIsNear = true;
} else { } else {
Log.d(TAG, "Proximity sensor => FAR state"); Log.d(TAG, "Proximity sensor => FAR viewState");
lastStateReportIsNear = false; lastStateReportIsNear = false;
} }
// Report about new state to listening client. Client can then call // Report about new viewState to listening client. Client can then call
// sensorReportsNearState() to query the current state (NEAR or FAR). // sensorReportsNearState() to query the current viewState (NEAR or FAR).
if (onSensorStateListener != null) { if (onSensorStateListener != null) {
onSensorStateListener.run(); onSensorStateListener.run();
} }

View File

@ -27,7 +27,7 @@
android:orientation="vertical"> android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view" android:id="@+id/recyclerView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_above="@id/bottom_navigation" android:layout_above="@id/bottom_navigation"
@ -53,7 +53,7 @@
layout="@layout/fast_scroller" layout="@layout/fast_scroller"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignTop="@id/recycler_view" android:layout_alignTop="@id/recyclerView"
android:layout_alignBottom="@id/recycler_view" /> android:layout_alignBottom="@id/recyclerView" />
</RelativeLayout> </RelativeLayout>

View File

@ -25,7 +25,7 @@
android:background="@color/nc_white_color"> android:background="@color/nc_white_color">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view" android:id="@+id/recyclerView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
tools:listitem="@layout/rv_item_conversation" /> tools:listitem="@layout/rv_item_conversation" />

View File

@ -106,7 +106,7 @@
tools:ignore="UnknownIdInLayout"> tools:ignore="UnknownIdInLayout">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view" android:id="@+id/recyclerView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
tools:listitem="@layout/rv_item_contact" /> tools:listitem="@layout/rv_item_contact" />

View File

@ -75,7 +75,7 @@
android:layout_height="match_parent"> android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view" android:id="@+id/recyclerView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:listitem="@layout/rv_item_conversation" /> tools:listitem="@layout/rv_item_conversation" />
@ -90,6 +90,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="bottom|end" android:layout_gravity="bottom|end"
android:layout_margin="16dp" android:layout_margin="16dp"
android:visibility="gone"
app:srcCompat="@drawable/ic_add_white_24px" /> app:srcCompat="@drawable/ic_add_white_24px" />
<include layout="@layout/fast_scroller" /> <include layout="@layout/fast_scroller" />

View File

@ -34,7 +34,7 @@
android:layout_height="match_parent"> android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view" android:id="@+id/recyclerView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:listitem="@layout/rv_item_conversation" /> tools:listitem="@layout/rv_item_conversation" />

View File

@ -27,6 +27,7 @@
android:title="@string/nc_search" android:title="@string/nc_search"
android:icon="@drawable/ic_search_white_24dp" android:icon="@drawable/ic_search_white_24dp"
app:showAsAction="collapseActionView|always" app:showAsAction="collapseActionView|always"
android:visible="false"
android:animateLayoutChanges="true" android:animateLayoutChanges="true"
app:actionViewClass="androidx.appcompat.widget.SearchView" app:actionViewClass="androidx.appcompat.widget.SearchView"
/> />

View File

@ -22,7 +22,7 @@
buildscript { buildscript {
ext { ext {
kotlinVersion = '1.3.50' kotlin_version = '1.3.50'
} }
repositories { repositories {
@ -34,7 +34,7 @@ buildscript {
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.5.1' classpath 'com.android.tools.build:gradle:3.5.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlin_version}"
classpath "io.realm:realm-gradle-plugin:6.0.0" classpath "io.realm:realm-gradle-plugin:6.0.0"
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong