From 8a3008ef25e53e8ddee7e15747c82e6785385ddf Mon Sep 17 00:00:00 2001 From: Mario Danic Date: Wed, 16 Oct 2019 13:42:11 +0200 Subject: [PATCH] Lots of progress on the conversations list view Signed-off-by: Mario Danic --- app/build.gradle | 38 +- .../nextcloud/talk/activities/MainActivity.kt | 5 +- .../talk/adapters/items/ConversationItem.java | 3 +- .../java/com/nextcloud/talk/api/NcApi.java | 4 +- .../application/NextcloudTalkApplication.kt | 3 +- .../controllers/BrowserController.java | 2 +- .../AccountVerificationController.java | 15 + .../talk/controllers/CallController.java | 14 + .../CallNotificationController.java | 12 + .../talk/controllers/ChatController.kt | 5 +- .../talk/controllers/ContactsController.java | 9 +- .../controllers/ConversationInfoController.kt | 14 +- .../ConversationsListController.java | 4 +- .../RingtoneSelectionController.java | 2 +- .../talk/controllers/SettingsController.java | 2 +- .../controllers/SwitchAccountController.java | 2 +- .../talk/controllers/base/BaseController.kt | 24 +- .../controllers/base/ButterKnifeController.kt | 2 +- .../bottomsheet/CallMenuController.java | 2 +- .../repository/NextcloudTalkRepositoryImpl.kt | 4 +- .../talk/newarch/di/module/NetworkModule.kt | 17 +- .../repository/NextcloudTalkRepository.kt | 2 +- .../usecases/GetConversationsUseCase.kt | 2 +- .../domain/usecases/base/BaseUseCase.kt | 1 + .../ConversationListViewModelFactory.kt} | 36 +- .../ConversationsListView.kt | 335 ++++++++++++++++++ .../ConversationsListViewModel.kt | 79 +++++ .../di/module/ConversationsListModule.kt | 47 +++ .../search/DebouncingQueryTextListener.kt | 57 +++ .../nextcloud/talk/newarch/mvvm/BaseView.kt | 83 +++++ .../BaseViewModel.kt} | 32 +- .../MvpPresenter.kt => mvvm/ViewState.kt} | 9 +- .../ext/RecyclerView.kt} | 23 +- .../talk/utils/ConductorRemapping.kt | 60 ++-- .../com/nextcloud/talk/utils/PushUtils.java | 2 +- .../talk/webrtc/MagicAudioManager.java | 28 +- .../talk/webrtc/MagicBluetoothManager.java | 78 ++-- .../talk/webrtc/MagicProximitySensor.java | 10 +- .../main/res/layout/controller_browser.xml | 6 +- .../main/res/layout/controller_call_menu.xml | 2 +- .../layout/controller_conversation_info.xml | 2 +- .../layout/controller_conversations_rv.xml | 3 +- .../main/res/layout/controller_generic_rv.xml | 2 +- .../menu/menu_conversation_plus_filter.xml | 1 + build.gradle | 4 +- 45 files changed, 890 insertions(+), 197 deletions(-) rename app/src/main/java/com/nextcloud/talk/newarch/{mvp/BaseMvpView.kt => features/conversationsList/ConversationListViewModelFactory.kt} (53%) create mode 100644 app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/ConversationsListView.kt create mode 100644 app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/ConversationsListViewModel.kt create mode 100644 app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/di/module/ConversationsListModule.kt create mode 100644 app/src/main/java/com/nextcloud/talk/newarch/features/search/DebouncingQueryTextListener.kt create mode 100644 app/src/main/java/com/nextcloud/talk/newarch/mvvm/BaseView.kt rename app/src/main/java/com/nextcloud/talk/newarch/{mvp/BasePresenter.kt => mvvm/BaseViewModel.kt} (60%) rename app/src/main/java/com/nextcloud/talk/newarch/{mvp/MvpPresenter.kt => mvvm/ViewState.kt} (86%) rename app/src/main/java/com/nextcloud/talk/newarch/{features/conversationsList/ConversationsListContract.kt => mvvm/ext/RecyclerView.kt} (67%) diff --git a/app/build.gradle b/app/build.gradle index 4b69bf2cd..a879b743a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -76,6 +76,10 @@ android { ] } } + + dataBinding { + enabled = true + } } dexOptions { @@ -136,8 +140,9 @@ android { } ext { - workVersion = "1.0.1" - koin_version = "2.0.1" + work_version = "1.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.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 'com.google.android.material:material:1.1.0-beta01' @@ -168,10 +192,10 @@ dependencies { implementation 'com.github.vanniktech:Emoji:0.6.0' implementation group: 'androidx.emoji', name: 'emoji-bundled', version: '1.0.0' implementation 'org.michaelevans.colorart:library:0.0.3' - implementation "android.arch.work:work-runtime:${workVersion}" - implementation "android.arch.work:work-rxjava2:${workVersion}" + implementation "android.arch.work:work-runtime:${work_version}" + implementation "android.arch.work:work-rxjava2:${work_version}" 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', { 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.biometric:biometric:1.0.0-beta02' + implementation 'androidx.biometric:biometric:1.0.0-rc01' implementation "androidx.lifecycle:lifecycle-extensions:2.1.0" 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-rxlifecycle2: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-urlconnection:4.2.2' @@ -226,6 +251,7 @@ dependencies { implementation 'com.github.HITGIF:TextFieldBoxes:1.4.5' implementation 'eu.davidea:flexible-adapter:5.1.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 'com.yarolegovich:lovely-dialog:1.1.0' implementation 'com.yarolegovich:lovelyinput:1.0.9' diff --git a/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt index 00a7f618d..7836ff2aa 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt @@ -43,6 +43,7 @@ import com.nextcloud.talk.controllers.ConversationsListController import com.nextcloud.talk.controllers.LockedController import com.nextcloud.talk.controllers.ServerSelectionController 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.SecurityUtils 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 (!router!!.hasRootController()) { - router!!.setRoot(RouterTransaction.with(ConversationsListController()) + router!!.setRoot(RouterTransaction.with(ConversationsListView()) .pushChangeHandler(HorizontalChangeHandler()) .popChangeHandler(HorizontalChangeHandler())) } @@ -99,7 +100,7 @@ class MainActivity : BaseActivity(), ActionBarProvider { } else if (!router!!.hasRootController()) { if (hasDb) { if (userUtils.anyUserExists()) { - router!!.setRoot(RouterTransaction.with(ConversationsListController()) + router!!.setRoot(RouterTransaction.with(ConversationsListView()) .pushChangeHandler(HorizontalChangeHandler()) .popChangeHandler(HorizontalChangeHandler())) } else { diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.java b/app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.java index 820975c86..95ef35e41 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.java +++ b/app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.java @@ -60,7 +60,7 @@ public class ConversationItem extends AbstractFlexibleItem adapter, ConversationItemViewHolder holder, int position, List payloads) { Context appContext = NextcloudTalkApplication.Companion.getSharedApplication().getApplicationContext(); + holder.dialogAvatar.setController(null); if (adapter.hasFilter()) { diff --git a/app/src/main/java/com/nextcloud/talk/api/NcApi.java b/app/src/main/java/com/nextcloud/talk/api/NcApi.java index f3f42e4a9..ca583b251 100644 --- a/app/src/main/java/com/nextcloud/talk/api/NcApi.java +++ b/app/src/main/java/com/nextcloud/talk/api/NcApi.java @@ -319,7 +319,7 @@ public interface NcApi { @FormUrlEncoded @PUT - Observable setReadOnlyState(@Header("Authorization") String authorization, @Url String url, @Field("state") int state); + Observable setReadOnlyState(@Header("Authorization") String authorization, @Url String url, @Field("viewState") int state); @FormUrlEncoded @@ -332,7 +332,7 @@ public interface NcApi { @FormUrlEncoded @PUT Observable setLobbyForConversation(@Header("Authorization") String authorization, - @Url String url, @Field("state") Integer state, + @Url String url, @Field("viewState") Integer state, @Field("timer") Long timer); } diff --git a/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt b/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt index 82ddd5208..9ab60366a 100644 --- a/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt +++ b/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt @@ -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.NetworkModule 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.DeviceUtils import com.nextcloud.talk.utils.DisplayUtils @@ -193,7 +194,7 @@ class NextcloudTalkApplication : Application(), LifecycleObserver { startKoin { androidContext(this@NextcloudTalkApplication) androidLogger() - modules(listOf(CommunicationModule, StorageModule, NetworkModule)) + modules(listOf(CommunicationModule, StorageModule, NetworkModule, ConversationsListModule)) } } diff --git a/app/src/main/java/com/nextcloud/talk/components/filebrowser/controllers/BrowserController.java b/app/src/main/java/com/nextcloud/talk/components/filebrowser/controllers/BrowserController.java index a9fdcc729..d788c56c7 100644 --- a/app/src/main/java/com/nextcloud/talk/components/filebrowser/controllers/BrowserController.java +++ b/app/src/main/java/com/nextcloud/talk/components/filebrowser/controllers/BrowserController.java @@ -75,7 +75,7 @@ public class BrowserController extends BaseController implements ListingInterfac private final Set selectedPaths; @Inject UserUtils userUtils; - @BindView(R.id.recycler_view) + @BindView(R.id.recyclerView) RecyclerView recyclerView; @BindView(R.id.fast_scroller) FastScroller fastScroller; diff --git a/app/src/main/java/com/nextcloud/talk/controllers/AccountVerificationController.java b/app/src/main/java/com/nextcloud/talk/controllers/AccountVerificationController.java index 98edbfda3..92addccf7 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/AccountVerificationController.java +++ b/app/src/main/java/com/nextcloud/talk/controllers/AccountVerificationController.java @@ -84,6 +84,9 @@ public class AccountVerificationController extends BaseController { @Inject AppPreferences appPreferences; + @Inject + EventBus eventBus; + @BindView(R.id.progress_text) TextView progressText; @@ -115,6 +118,18 @@ public class AccountVerificationController extends BaseController { 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 protected void onViewBound(@NonNull View view) { super.onViewBound(view); diff --git a/app/src/main/java/com/nextcloud/talk/controllers/CallController.java b/app/src/main/java/com/nextcloud/talk/controllers/CallController.java index a12318932..20c67a0a8 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/CallController.java +++ b/app/src/main/java/com/nextcloud/talk/controllers/CallController.java @@ -197,6 +197,8 @@ public class CallController extends BaseController { AppPreferences appPreferences; @Inject Cache cache; + @Inject + EventBus eventBus; private PeerConnectionFactory peerConnectionFactory; 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) public void onMessageEvent(WebSocketCommunicationEvent webSocketCommunicationEvent) { switch (webSocketCommunicationEvent.getType()) { diff --git a/app/src/main/java/com/nextcloud/talk/controllers/CallNotificationController.java b/app/src/main/java/com/nextcloud/talk/controllers/CallNotificationController.java index 4e9e1243a..8dfa6254b 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/CallNotificationController.java +++ b/app/src/main/java/com/nextcloud/talk/controllers/CallNotificationController.java @@ -160,6 +160,18 @@ public class CallNotificationController extends BaseController { 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() { callAnswerCameraView.setVisibility(View.VISIBLE); callAnswerVoiceOnlyView.setVisibility(View.VISIBLE); diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt b/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt index b2b2c93a5..d5a7ae0e3 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt @@ -654,6 +654,7 @@ class ChatController(args: Bundle) : BaseController(), MessagesListAdapter override fun onAttach(view: View) { super.onAttach(view) + eventBus.register(this) if (conversationUser?.userId != "?" && conversationUser?.hasSpreedFeatureCapability( "mention-flag" @@ -728,7 +729,7 @@ class ChatController(args: Bundle) : BaseController(), MessagesListAdapter } override fun onDetach(view: View) { - super.onDetach(view) + eventBus.unregister(this) ApplicationWideCurrentRoomHolder.getInstance() .clear() @@ -747,6 +748,8 @@ class ChatController(args: Bundle) : BaseController(), MessagesListAdapter if (mentionAutocomplete != null && mentionAutocomplete!!.isPopupShowing) { mentionAutocomplete?.dismissPopup() } + + super.onDetach(view) } override fun getTitle(): String? { diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ContactsController.java b/app/src/main/java/com/nextcloud/talk/controllers/ContactsController.java index 60c2be3da..c1dd50040 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ContactsController.java +++ b/app/src/main/java/com/nextcloud/talk/controllers/ContactsController.java @@ -128,7 +128,7 @@ public class ContactsController extends BaseController implements SearchView.OnQ AppPreferences appPreferences; @BindView(R.id.progressBar) ProgressBar progressBar; - @BindView(R.id.recycler_view) + @BindView(R.id.recyclerView) RecyclerView recyclerView; @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); } + @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); if (isNewConversationView) { toggleNewCallHeaderVisibility(!isPublicCall); diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt b/app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt index a3464dd9d..cd834f59c 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt @@ -20,7 +20,7 @@ package com.nextcloud.talk.controllers -import android.content.Context +import android.content.res.Configuration import android.graphics.drawable.Drawable import android.graphics.drawable.LayerDrawable import android.os.Bundle @@ -85,7 +85,6 @@ import io.reactivex.Observer import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers -import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import java.util.ArrayList @@ -94,7 +93,6 @@ import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) class ConversationInfoController(args: Bundle) : BaseController(), FlexibleAdapter.OnItemClickListener { - @BindView(R.id.notification_settings) lateinit var notificationsPreferenceScreen: MaterialPreferenceScreen @BindView(R.id.progressBar) @@ -115,7 +113,7 @@ class ConversationInfoController(args: Bundle) : BaseController(), FlexibleAdapt lateinit var conversationDisplayName: EmojiTextView @BindView(R.id.participants_list_category) lateinit var participantsListCategory: MaterialPreferenceCategoryWithRightLink - @BindView(R.id.recycler_view) + @BindView(R.id.recyclerView) lateinit var recyclerView: RecyclerView @BindView(R.id.deleteConversationAction) lateinit var deleteConversationAction: MaterialStandardPreference @@ -179,8 +177,14 @@ class ConversationInfoController(args: Bundle) : BaseController(), FlexibleAdapt 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) { super.onAttach(view) + eventBus.register(this) if (databaseStorageModule == null) { databaseStorageModule = DatabaseStorageModule(conversationUser!!, conversationToken) @@ -327,7 +331,7 @@ class ConversationInfoController(args: Bundle) : BaseController(), FlexibleAdapt super.onRestoreViewState(view, savedViewState) if (LovelySaveStateHandler.wasDialogOnScreen(savedViewState)) { //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) } } diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.java b/app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.java index 895b8c601..11537d5d2 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.java +++ b/app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.java @@ -133,7 +133,7 @@ public class ConversationsListController extends BaseController implements Searc @Inject AppPreferences appPreferences; - @BindView(R.id.recycler_view) + @BindView(R.id.recyclerView) RecyclerView recyclerView; @BindView(R.id.swipeRefreshLayoutView) @@ -480,7 +480,7 @@ public class ConversationsListController extends BaseController implements Searc searchQuery = savedViewState.getString(KEY_SEARCH_QUERY, ""); if (LovelySaveStateHandler.wasDialogOnScreen(savedViewState)) { //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); } } diff --git a/app/src/main/java/com/nextcloud/talk/controllers/RingtoneSelectionController.java b/app/src/main/java/com/nextcloud/talk/controllers/RingtoneSelectionController.java index 78c5d0099..39f2290e7 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/RingtoneSelectionController.java +++ b/app/src/main/java/com/nextcloud/talk/controllers/RingtoneSelectionController.java @@ -61,7 +61,7 @@ public class RingtoneSelectionController extends BaseController implements Flexi private static final String TAG = "RingtoneSelectionController"; - @BindView(R.id.recycler_view) + @BindView(R.id.recyclerView) RecyclerView recyclerView; @BindView(R.id.swipe_refresh_layout) diff --git a/app/src/main/java/com/nextcloud/talk/controllers/SettingsController.java b/app/src/main/java/com/nextcloud/talk/controllers/SettingsController.java index 5fe4ec3c4..69acad333 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/SettingsController.java +++ b/app/src/main/java/com/nextcloud/talk/controllers/SettingsController.java @@ -346,7 +346,7 @@ public class SettingsController extends BaseController { super.onRestoreViewState(view, savedViewState); if (LovelySaveStateHandler.wasDialogOnScreen(savedViewState)) { //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); } } diff --git a/app/src/main/java/com/nextcloud/talk/controllers/SwitchAccountController.java b/app/src/main/java/com/nextcloud/talk/controllers/SwitchAccountController.java index 62a48cf93..6663e301c 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/SwitchAccountController.java +++ b/app/src/main/java/com/nextcloud/talk/controllers/SwitchAccountController.java @@ -64,7 +64,7 @@ public class SwitchAccountController extends BaseController { @Inject UserUtils userUtils; - @BindView(R.id.recycler_view) + @BindView(R.id.recyclerView) RecyclerView recyclerView; @Inject diff --git a/app/src/main/java/com/nextcloud/talk/controllers/base/BaseController.kt b/app/src/main/java/com/nextcloud/talk/controllers/base/BaseController.kt index 32d5d3f95..72ec94f10 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/base/BaseController.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/base/BaseController.kt @@ -21,10 +21,11 @@ */ package com.nextcloud.talk.controllers.base +import android.content.ComponentCallbacks import android.content.Context +import android.content.res.Configuration import android.os.Build import android.util.Log -import android.view.LayoutInflater import android.view.MenuItem import android.view.View 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.uber.autodispose.lifecycle.LifecycleScopeProvider import org.greenrobot.eventbus.EventBus -import org.koin.core.Koin -import org.koin.core.KoinComponent -import org.koin.core.inject -import org.koin.core.qualifier.named -import org.koin.core.scope.Scope +import org.koin.android.ext.android.inject import java.util.ArrayList -import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) -abstract class BaseController : ButterKnifeController(), KoinComponent { +abstract class BaseController : ButterKnifeController(), ComponentCallbacks { val scopeProvider: LifecycleScopeProvider<*> = ControllerScopeProvider.from(this) @@ -75,10 +71,6 @@ abstract class BaseController : ButterKnifeController(), KoinComponent { return actionBarProvider?.supportActionBar } - override fun getKoin(): Koin { - return super.getKoin() - } - override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { android.R.id.home -> { @@ -112,7 +104,6 @@ abstract class BaseController : ButterKnifeController(), KoinComponent { override fun onAttach(view: View) { super.onAttach(view) - eventBus.register(this) setTitle() if (actionBar != null) { @@ -121,7 +112,6 @@ abstract class BaseController : ButterKnifeController(), KoinComponent { } override fun onDetach(view: View) { - eventBus.unregister(this) val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager imm.hideSoftInputFromWindow(view.windowToken, 0) super.onDetach(view) @@ -159,6 +149,12 @@ abstract class BaseController : ButterKnifeController(), KoinComponent { } } + override fun onLowMemory() { + } + + override fun onConfigurationChanged(newConfig: Configuration) { + } + companion object { private val TAG = "BaseController" diff --git a/app/src/main/java/com/nextcloud/talk/controllers/base/ButterKnifeController.kt b/app/src/main/java/com/nextcloud/talk/controllers/base/ButterKnifeController.kt index 630219e24..b4d06edb6 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/base/ButterKnifeController.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/base/ButterKnifeController.kt @@ -31,7 +31,7 @@ import com.bluelinelabs.conductor.Controller abstract class ButterKnifeController : Controller { - private var unbinder: Unbinder? = null + protected var unbinder: Unbinder? = null constructor() {} diff --git a/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/CallMenuController.java b/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/CallMenuController.java index e963daf55..9d4ed53e4 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/CallMenuController.java +++ b/app/src/main/java/com/nextcloud/talk/controllers/bottomsheet/CallMenuController.java @@ -67,7 +67,7 @@ import org.parceler.Parcels; @AutoInjector(NextcloudTalkApplication.class) public class CallMenuController extends BaseController implements FlexibleAdapter.OnItemClickListener { - @BindView(R.id.recycler_view) + @BindView(R.id.recyclerView) RecyclerView recyclerView; @Inject diff --git a/app/src/main/java/com/nextcloud/talk/newarch/data/repository/NextcloudTalkRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/newarch/data/repository/NextcloudTalkRepositoryImpl.kt index 8ecadde7e..8a265b576 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/data/repository/NextcloudTalkRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/data/repository/NextcloudTalkRepositoryImpl.kt @@ -27,8 +27,8 @@ import com.nextcloud.talk.newarch.domain.repository.NextcloudTalkRepository import com.nextcloud.talk.utils.ApiUtils class NextcloudTalkRepositoryImpl(private val apiService: ApiService) : NextcloudTalkRepository { - override suspend fun getConversations(user: UserEntity): List { + override suspend fun getConversationsForUser(user: UserEntity): List { return apiService.getConversations(ApiUtils.getCredentials(user.username, user.token), ApiUtils.getUrlForGetRooms(user.baseUrl)).ocs.data } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/nextcloud/talk/newarch/di/module/NetworkModule.kt b/app/src/main/java/com/nextcloud/talk/newarch/di/module/NetworkModule.kt index dd61b9c75..0f2a6891f 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/di/module/NetworkModule.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/di/module/NetworkModule.kt @@ -50,6 +50,7 @@ import okhttp3.OkHttpClient import okhttp3.internal.tls.OkHostnameVerifier import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor.Logger +import org.koin.android.ext.koin.androidApplication import org.koin.android.ext.koin.androidContext import org.koin.dsl.module import retrofit2.Retrofit @@ -70,8 +71,16 @@ import javax.net.ssl.X509KeyManager val NetworkModule = module { single { createService(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()) } - factory { createGetConversationsUseCase(get(), get()) } + + single { createNextcloudTalkRepository(get()) } } fun createCookieManager(): CookieManager { @@ -242,9 +251,3 @@ fun createNextcloudTalkRepository(apiService: ApiService): NextcloudTalkReposito return NextcloudTalkRepositoryImpl(apiService) } -fun createGetConversationsUseCase( - nextcloudTalkRepository: NextcloudTalkRepository, - apiErrorHandler: ApiErrorHandler -): GetConversationsUseCase { - return GetConversationsUseCase(nextcloudTalkRepository, apiErrorHandler) -} diff --git a/app/src/main/java/com/nextcloud/talk/newarch/domain/repository/NextcloudTalkRepository.kt b/app/src/main/java/com/nextcloud/talk/newarch/domain/repository/NextcloudTalkRepository.kt index 6241f7c32..c3e777117 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/domain/repository/NextcloudTalkRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/domain/repository/NextcloudTalkRepository.kt @@ -24,5 +24,5 @@ import com.nextcloud.talk.models.database.UserEntity import com.nextcloud.talk.models.json.conversations.Conversation interface NextcloudTalkRepository { - suspend fun getConversations(user: UserEntity): List + suspend fun getConversationsForUser(user: UserEntity): List } diff --git a/app/src/main/java/com/nextcloud/talk/newarch/domain/usecases/GetConversationsUseCase.kt b/app/src/main/java/com/nextcloud/talk/newarch/domain/usecases/GetConversationsUseCase.kt index b8bdec069..05d03e089 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/domain/usecases/GetConversationsUseCase.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/domain/usecases/GetConversationsUseCase.kt @@ -31,6 +31,6 @@ class GetConversationsUseCase constructor( ) : UseCase, Any?>(apiErrorHandler) { override suspend fun run(params: Any?): List { - return nextcloudTalkRepository.getConversations(user); + return nextcloudTalkRepository.getConversationsForUser(user); } } diff --git a/app/src/main/java/com/nextcloud/talk/newarch/domain/usecases/base/BaseUseCase.kt b/app/src/main/java/com/nextcloud/talk/newarch/domain/usecases/base/BaseUseCase.kt index 4df78277c..e96b01289 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/domain/usecases/base/BaseUseCase.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/domain/usecases/base/BaseUseCase.kt @@ -31,6 +31,7 @@ abstract class UseCase(private val apiErrorHandler: ApiErrorHan abstract suspend fun run(params: Params? = null): Type lateinit var user: UserEntity + fun isUserInitialized() = ::user.isInitialized fun invoke( scope: CoroutineScope, diff --git a/app/src/main/java/com/nextcloud/talk/newarch/mvp/BaseMvpView.kt b/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/ConversationListViewModelFactory.kt similarity index 53% rename from app/src/main/java/com/nextcloud/talk/newarch/mvp/BaseMvpView.kt rename to app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/ConversationListViewModelFactory.kt index 25683f96b..57650fd5c 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/mvp/BaseMvpView.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/ConversationListViewModelFactory.kt @@ -18,29 +18,19 @@ * along with this program. If not, see . */ -package com.nextcloud.talk.newarch.conversationsList.mvp +package com.nextcloud.talk.newarch.features.conversationsList -import android.view.View -import androidx.annotation.LayoutRes -import autodagger.AutoInjector -import com.nextcloud.talk.application.NextcloudTalkApplication -import com.nextcloud.talk.controllers.base.BaseController +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.nextcloud.talk.newarch.domain.usecases.GetConversationsUseCase +import com.nextcloud.talk.utils.database.user.UserUtils -@AutoInjector(NextcloudTalkApplication::class) -abstract class BaseView : BaseController() { +class ConversationListViewModelFactory constructor( + private val conversationsUseCase: GetConversationsUseCase, + private val userUtils: UserUtils +): ViewModelProvider.Factory { - override fun onDetach(view: View) { - super.onDetach(view) - getPresenter().stop() - } - - override fun onDestroy() { - getPresenter().destroy() - super.onDestroy() - } - - @LayoutRes - protected abstract fun getLayoutId(): Int - - protected abstract fun getPresenter(): MvpPresenter -} \ No newline at end of file + override fun create(modelClass: Class): T { + return ConversationsListViewModel(conversationsUseCase, userUtils) as T; + } +} diff --git a/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/ConversationsListView.kt b/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/ConversationsListView.kt new file mode 100644 index 000000000..d1fafa2f7 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/ConversationsListView.kt @@ -0,0 +1,335 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * Copyright (C) 2017-2019 Mario Danic + * + * 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 . + */ + +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() + 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() + for (conversation in it) { + newConversations.add(ConversationItem(conversation, viewModel.currentUser, activity)) + } + + recyclerViewAdapter.updateDataSet(newConversations as List>?) + + 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>) { + 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)) + bundle.putParcelable(BundleKeys.KEY_MENU_TYPE, Parcels.wrap(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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/ConversationsListViewModel.kt b/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/ConversationsListViewModel.kt new file mode 100644 index 000000000..11e07106c --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/ConversationsListViewModel.kt @@ -0,0 +1,79 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * Copyright (C) 2017-2019 Mario Danic + * + * 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 . + */ + +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() { + + val conversationsListData = MutableLiveData>() + val viewState = MutableLiveData(LOADING) + val messageData = MutableLiveData() + val searchQuery = MutableLiveData() + 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> { + override fun onSuccess(result: List) { + 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 + } + + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/di/module/ConversationsListModule.kt b/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/di/module/ConversationsListModule.kt new file mode 100644 index 000000000..c64517da9 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/di/module/ConversationsListModule.kt @@ -0,0 +1,47 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * Copyright (C) 2017-2019 Mario Danic + * + * 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 . + */ + +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) +} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/newarch/features/search/DebouncingQueryTextListener.kt b/app/src/main/java/com/nextcloud/talk/newarch/features/search/DebouncingQueryTextListener.kt new file mode 100644 index 000000000..e996de490 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/newarch/features/search/DebouncingQueryTextListener.kt @@ -0,0 +1,57 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * Copyright (C) 2017-2019 Mario Danic + * + * 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 . + * + * 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/newarch/mvvm/BaseView.kt b/app/src/main/java/com/nextcloud/talk/newarch/mvvm/BaseView.kt new file mode 100644 index 000000000..95e4f23ae --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/newarch/mvvm/BaseView.kt @@ -0,0 +1,83 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * Copyright (C) 2017-2019 Mario Danic + * + * 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 . + */ + +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 +} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/newarch/mvp/BasePresenter.kt b/app/src/main/java/com/nextcloud/talk/newarch/mvvm/BaseViewModel.kt similarity index 60% rename from app/src/main/java/com/nextcloud/talk/newarch/mvp/BasePresenter.kt rename to app/src/main/java/com/nextcloud/talk/newarch/mvvm/BaseViewModel.kt index 150d4b1cd..d783ba237 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/mvp/BasePresenter.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/mvvm/BaseViewModel.kt @@ -20,23 +20,29 @@ package com.nextcloud.talk.newarch.conversationsList.mvp +import androidx.lifecycle.ViewModel import io.reactivex.disposables.CompositeDisposable +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel -abstract class BasePresenter : MvpPresenter { +abstract class BaseViewModel : ViewModel() { - protected val disposables: CompositeDisposable = CompositeDisposable() - protected var view: V? = null - private set + protected val disposables: CompositeDisposable = CompositeDisposable() - fun start(view: V) { - this.view = view - } + val backgroundAndUIScope = CoroutineScope( + Job() + Dispatchers.Main + ) - override fun stop() { - this.view = null - } + val backgroundScope = CoroutineScope( + Job() + ) - override fun destroy() { - disposables.clear() - } + override fun onCleared() { + super.onCleared() + disposables.clear() + backgroundAndUIScope.cancel() + backgroundScope.cancel() + } } \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/newarch/mvp/MvpPresenter.kt b/app/src/main/java/com/nextcloud/talk/newarch/mvvm/ViewState.kt similarity index 86% rename from app/src/main/java/com/nextcloud/talk/newarch/mvp/MvpPresenter.kt rename to app/src/main/java/com/nextcloud/talk/newarch/mvvm/ViewState.kt index 1eb140889..f79a946b2 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/mvp/MvpPresenter.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/mvvm/ViewState.kt @@ -18,9 +18,10 @@ * along with this program. If not, see . */ -package com.nextcloud.talk.newarch.conversationsList.mvp +package com.nextcloud.talk.newarch.mvvm -interface MvpPresenter { - fun stop() - fun destroy() +enum class ViewState { + LOADING, + LOADED, + FAILED } \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/ConversationsListContract.kt b/app/src/main/java/com/nextcloud/talk/newarch/mvvm/ext/RecyclerView.kt similarity index 67% rename from app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/ConversationsListContract.kt rename to app/src/main/java/com/nextcloud/talk/newarch/mvvm/ext/RecyclerView.kt index 755afe5a5..1f9ed2e76 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/ConversationsListContract.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/mvvm/ext/RecyclerView.kt @@ -18,17 +18,16 @@ * along with this program. If not, see . */ -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 { - interface View { - fun onLoadConversationsSuccess(conversations: List) - fun onLoadConversationsFailure(throwable: Throwable) - } - - interface Presenter { - fun loadConversations() - } -} +fun RecyclerView.initRecyclerView( + layoutManager: RecyclerView.LayoutManager, + adapter: RecyclerView.Adapter, + hasFixedSize: Boolean = true +) { + this.layoutManager = layoutManager + this.adapter = adapter + setHasFixedSize(hasFixedSize) +} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/utils/ConductorRemapping.kt b/app/src/main/java/com/nextcloud/talk/utils/ConductorRemapping.kt index e2845a0b7..db5abd146 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/ConductorRemapping.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/ConductorRemapping.kt @@ -27,31 +27,41 @@ import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler import com.nextcloud.talk.controllers.ChatController object ConductorRemapping { - fun remapChatController(router: Router, internalUserId: Long, roomTokenOrId: String, bundle: Bundle, replaceTop: Boolean) { - val tag = "$internalUserId@$roomTokenOrId" - if (router.getControllerWithTag(tag) != null) { - val backstack = router.backstack - var routerTransaction: RouterTransaction? = null - for (i in 0 until router.backstackSize) { - if (tag == backstack[i].tag()) { - routerTransaction = backstack[i] - backstack.remove(routerTransaction) - break - } - } - - backstack.add(routerTransaction) - router.setBackstack(backstack, HorizontalChangeHandler()) - } else { - if (!replaceTop) { - router.pushController(RouterTransaction.with(ChatController(bundle)) - .pushChangeHandler(HorizontalChangeHandler()) - .popChangeHandler(HorizontalChangeHandler()).tag(tag)) - } else { - router.replaceTopController(RouterTransaction.with(ChatController(bundle)) - .pushChangeHandler(HorizontalChangeHandler()) - .popChangeHandler(HorizontalChangeHandler()).tag(tag)) - } + fun remapChatController( + router: Router, + internalUserId: Long, + roomTokenOrId: String, + bundle: Bundle, + replaceTop: Boolean + ) { + val tag = "$internalUserId@$roomTokenOrId" + if (router.getControllerWithTag(tag) != null) { + val backstack = router.backstack + var routerTransaction: RouterTransaction? = null + for (i in 0 until router.backstackSize) { + if (tag == backstack[i].tag()) { + routerTransaction = backstack[i] + backstack.remove(routerTransaction) + break } + } + + backstack.add(routerTransaction) + router.setBackstack(backstack, HorizontalChangeHandler()) + } else { + if (!replaceTop) { + router.pushController( + RouterTransaction.with(ChatController(bundle)) + .pushChangeHandler(HorizontalChangeHandler()) + .popChangeHandler(HorizontalChangeHandler()).tag(tag) + ) + } else { + router.replaceTopController( + RouterTransaction.with(ChatController(bundle)) + .pushChangeHandler(HorizontalChangeHandler()) + .popChangeHandler(HorizontalChangeHandler()).tag(tag) + ) + } } + } } diff --git a/app/src/main/java/com/nextcloud/talk/utils/PushUtils.java b/app/src/main/java/com/nextcloud/talk/utils/PushUtils.java index f9a717f38..652668664 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/PushUtils.java +++ b/app/src/main/java/com/nextcloud/talk/utils/PushUtils.java @@ -128,7 +128,7 @@ public class PushUtils { } catch (NoSuchAlgorithmException e) { Log.d(TAG, "No such algorithm"); } 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) { Log.d(TAG, "Invalid key while trying to verify"); } catch (SignatureException e) { diff --git a/app/src/main/java/com/nextcloud/talk/webrtc/MagicAudioManager.java b/app/src/main/java/com/nextcloud/talk/webrtc/MagicAudioManager.java index 3cbfcd60e..c76452338 100644 --- a/app/src/main/java/com/nextcloud/talk/webrtc/MagicAudioManager.java +++ b/app/src/main/java/com/nextcloud/talk/webrtc/MagicAudioManager.java @@ -125,7 +125,7 @@ public class MagicAudioManager { // Tablet devices (e.g. Nexus 7) does not support proximity sensors. // Note that, the sensor will not be active until start() has been called. 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), // or removes his hand from the device. 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". */ private void onProximitySensorChangedState() { @@ -205,7 +205,7 @@ public class MagicAudioManager { this.audioManagerEvents = audioManagerEvents; 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(); savedIsSpeakerPhoneOn = audioManager.isSpeakerphoneOn(); savedIsMicrophoneMute = audioManager.isMicrophoneMute(); @@ -294,7 +294,7 @@ public class MagicAudioManager { Log.d(TAG, "stop"); ThreadUtils.checkIsOnMainThread(); 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; } 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) { boolean wasMuted = audioManager.isMicrophoneMute(); @@ -445,7 +445,7 @@ public class MagicAudioManager { } /** - * Gets the current earpiece state. + * Gets the current earpiece viewState. */ private boolean hasEarpiece() { 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. - * TODO(henrika): add unit test to verify all state transitions. + * TODO(henrika): add unit test to verify all viewState transitions. */ public void updateAudioDeviceState() { ThreadUtils.checkIsOnMainThread(); Log.d(TAG, "--- updateAudioDeviceState: " + "wired headset=" + hasWiredHeadset + ", " - + "BT state=" + bluetoothManager.getState()); + + "BT viewState=" + bluetoothManager.getState()); Log.d(TAG, "Device status: " + "available=" + audioDevices + ", " + "selected=" + selectedAudioDevice + ", " + "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. - // 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 || bluetoothManager.getState() == MagicBluetoothManager.State.HEADSET_UNAVAILABLE || bluetoothManager.getState() == MagicBluetoothManager.State.SCO_DISCONNECTING) { @@ -521,7 +521,7 @@ public class MagicAudioManager { 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); // Update the existing audio device set. audioDevices = newAudioDevices; @@ -562,7 +562,7 @@ public class MagicAudioManager { || bluetoothManager.getState() == MagicBluetoothManager.State.SCO_CONNECTED) { Log.d(TAG, "Need BT audio: start=" + needBluetoothAudioStart + ", " + "stop=" + needBluetoothAudioStop + ", " - + "BT state=" + bluetoothManager.getState()); + + "BT viewState=" + bluetoothManager.getState()); } // Start or stop Bluetooth SCO connection given states set earlier. @@ -624,7 +624,7 @@ public class MagicAudioManager { } /** - * AudioManager state. + * AudioManager viewState. */ public enum AudioManagerState { UNINITIALIZED, @@ -649,7 +649,7 @@ public class MagicAudioManager { @Override 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); // String name = intent.getStringExtra("name"); hasWiredHeadset = (state == STATE_PLUGGED); diff --git a/app/src/main/java/com/nextcloud/talk/webrtc/MagicBluetoothManager.java b/app/src/main/java/com/nextcloud/talk/webrtc/MagicBluetoothManager.java index 5f49c0705..e87e2bd13 100644 --- a/app/src/main/java/com/nextcloud/talk/webrtc/MagicBluetoothManager.java +++ b/app/src/main/java/com/nextcloud/talk/webrtc/MagicBluetoothManager.java @@ -98,7 +98,7 @@ public class MagicBluetoothManager { } /** - * Returns the internal state. + * Returns the internal viewState. */ public State getState() { ThreadUtils.checkIsOnMainThread(); @@ -110,14 +110,14 @@ public class MagicBluetoothManager { /** * Activates components required to detect Bluetooth devices and to enable * 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 - * will start a state change sequence where the final outcome depends on + * viewState will be HEADSET_UNAVAILABLE but a viewState machine has started which + * will start a viewState change sequence where the final outcome depends on * 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: * UNINITIALIZED --> HEADSET_UNAVAILABLE --> HEADSET_AVAILABLE --> * 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. */ public void start() { @@ -128,7 +128,7 @@ public class MagicBluetoothManager { return; } if (bluetoothState != State.UNINITIALIZED) { - Log.w(TAG, "Invalid BT state"); + Log.w(TAG, "Invalid BT viewState"); return; } bluetoothHeadset = null; @@ -155,16 +155,16 @@ public class MagicBluetoothManager { } // Register receivers for BluetoothHeadset change notifications. 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); - // 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); registerReceiver(bluetoothHeadsetReceiver, bluetoothHeadsetFilter); - Log.d(TAG, "HEADSET profile state: " + Log.d(TAG, "HEADSET profile viewState: " + stateToString(bluetoothAdapter.getProfileConnectionState(BluetoothProfile.HEADSET))); Log.d(TAG, "Bluetooth proxy for headset profile has started"); 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() { ThreadUtils.checkIsOnMainThread(); - Log.d(TAG, "stop: BT state=" + bluetoothState); + Log.d(TAG, "stop: BT viewState=" + bluetoothState); if (bluetoothAdapter == null) { return; } @@ -191,7 +191,7 @@ public class MagicBluetoothManager { bluetoothAdapter = null; bluetoothDevice = null; 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() { ThreadUtils.checkIsOnMainThread(); - Log.d(TAG, "startSco: BT state=" + bluetoothState + ", " + Log.d(TAG, "startSco: BT viewState=" + bluetoothState + ", " + "attempts: " + scoConnectionAttempts + ", " + "SCO is on: " + isScoOn()); 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..."); // 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 - // 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; audioManager.startBluetoothSco(); audioManager.setBluetoothScoOn(true); scoConnectionAttempts++; startTimer(); - Log.d(TAG, "startScoAudio done: BT state=" + bluetoothState + ", " + Log.d(TAG, "startScoAudio done: BT viewState=" + bluetoothState + ", " + "SCO is on: " + isScoOn()); return true; } @@ -240,7 +240,7 @@ public class MagicBluetoothManager { */ public void stopScoAudio() { ThreadUtils.checkIsOnMainThread(); - Log.d(TAG, "stopScoAudio: BT state=" + bluetoothState + ", " + Log.d(TAG, "stopScoAudio: BT viewState=" + bluetoothState + ", " + "SCO is on: " + isScoOn()); if (bluetoothState != State.SCO_CONNECTING && bluetoothState != State.SCO_CONNECTED) { return; @@ -249,14 +249,14 @@ public class MagicBluetoothManager { audioManager.stopBluetoothSco(); audioManager.setBluetoothScoOn(false); bluetoothState = State.SCO_DISCONNECTING; - Log.d(TAG, "stopScoAudio done: BT state=" + bluetoothState + ", " + Log.d(TAG, "stopScoAudio done: BT viewState=" + bluetoothState + ", " + "SCO is on: " + isScoOn()); } /** * Use the BluetoothHeadset proxy object (controls the Bluetooth 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 * device if available. */ @@ -266,7 +266,7 @@ public class MagicBluetoothManager { } Log.d(TAG, "updateDevice"); // 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. List devices = bluetoothHeadset.getConnectedDevices(); if (devices.isEmpty()) { @@ -279,10 +279,10 @@ public class MagicBluetoothManager { bluetoothState = State.HEADSET_AVAILABLE; Log.d(TAG, "Connected bluetooth headset: " + "name=" + bluetoothDevice.getName() + ", " - + "state=" + stateToString(bluetoothHeadset.getConnectionState(bluetoothDevice)) + + "viewState=" + stateToString(bluetoothHeadset.getConnectionState(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") protected void logBluetoothAdapterInfo(BluetoothAdapter localAdapter) { Log.d(TAG, "BluetoothAdapter: " + "enabled=" + localAdapter.isEnabled() + ", " - + "state=" + stateToString(localAdapter.getState()) + ", " + + "viewState=" + stateToString(localAdapter.getState()) + ", " + "name=" + localAdapter.getName() + ", " + "address=" + localAdapter.getAddress()); // 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) { return; } - Log.d(TAG, "bluetoothTimeout: BT state=" + bluetoothState + ", " + Log.d(TAG, "bluetoothTimeout: BT viewState=" + bluetoothState + ", " + "attempts: " + scoConnectionAttempts + ", " + "SCO is on: " + isScoOn()); if (bluetoothState != State.SCO_CONNECTING) { @@ -385,7 +385,7 @@ public class MagicBluetoothManager { } } 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; scoConnectionAttempts = 0; } else { @@ -394,7 +394,7 @@ public class MagicBluetoothManager { stopScoAudio(); } 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 { // Bluetooth is not available; no adapter or Bluetooth is off. UNINITIALIZED, @@ -461,17 +461,17 @@ public class MagicBluetoothManager { private class BluetoothServiceListener implements BluetoothProfile.ServiceListener { @Override // 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. public void onServiceConnected(int profile, BluetoothProfile proxy) { if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) { 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. bluetoothHeadset = (BluetoothHeadset) proxy; updateAudioDeviceState(); - Log.d(TAG, "onServiceConnected done: BT state=" + bluetoothState); + Log.d(TAG, "onServiceConnected done: BT viewState=" + bluetoothState); } @Override @@ -480,18 +480,18 @@ public class MagicBluetoothManager { if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) { return; } - Log.d(TAG, "BluetoothServiceListener.onServiceDisconnected: BT state=" + bluetoothState); + Log.d(TAG, "BluetoothServiceListener.onServiceDisconnected: BT viewState=" + bluetoothState); stopScoAudio(); bluetoothHeadset = null; bluetoothDevice = null; bluetoothState = State.HEADSET_UNAVAILABLE; 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. - // Detects headset changes and Bluetooth SCO state changes. + // Detects headset changes and Bluetooth SCO viewState changes. private class BluetoothHeadsetBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { @@ -499,7 +499,7 @@ public class MagicBluetoothManager { return; } 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 // audio to BT over SCO. Typically received when user turns on a BT // headset while audio is active using another audio device. @@ -510,7 +510,7 @@ public class MagicBluetoothManager { + "a=ACTION_CONNECTION_STATE_CHANGED, " + "s=" + stateToString(state) + ", " + "sb=" + isInitialStickyBroadcast() + ", " - + "BT state: " + bluetoothState); + + "BT viewState: " + bluetoothState); if (state == BluetoothHeadset.STATE_CONNECTED) { scoConnectionAttempts = 0; updateAudioDeviceState(); @@ -523,7 +523,7 @@ public class MagicBluetoothManager { stopScoAudio(); 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. } else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) { final int state = intent.getIntExtra( @@ -532,7 +532,7 @@ public class MagicBluetoothManager { + "a=ACTION_AUDIO_STATE_CHANGED, " + "s=" + stateToString(state) + ", " + "sb=" + isInitialStickyBroadcast() + ", " - + "BT state: " + bluetoothState); + + "BT viewState: " + bluetoothState); if (state == BluetoothHeadset.STATE_AUDIO_CONNECTED) { cancelTimer(); if (bluetoothState == State.SCO_CONNECTING) { @@ -541,7 +541,7 @@ public class MagicBluetoothManager { scoConnectionAttempts = 0; updateAudioDeviceState(); } 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) { Log.d(TAG, "+++ Bluetooth audio SCO is now connecting..."); @@ -554,7 +554,7 @@ public class MagicBluetoothManager { updateAudioDeviceState(); } } - Log.d(TAG, "onReceive done: BT state=" + bluetoothState); + Log.d(TAG, "onReceive done: BT viewState=" + bluetoothState); } } } diff --git a/app/src/main/java/com/nextcloud/talk/webrtc/MagicProximitySensor.java b/app/src/main/java/com/nextcloud/talk/webrtc/MagicProximitySensor.java index 364820e06..f7ab61a47 100644 --- a/app/src/main/java/com/nextcloud/talk/webrtc/MagicProximitySensor.java +++ b/app/src/main/java/com/nextcloud/talk/webrtc/MagicProximitySensor.java @@ -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() { threadChecker.checkIsOnValidThread(); @@ -124,15 +124,15 @@ public class MagicProximitySensor implements SensorEventListener { // avoid blocking. float distanceInCentimeters = event.values[0]; if (distanceInCentimeters < proximitySensor.getMaximumRange()) { - Log.d(TAG, "Proximity sensor => NEAR state"); + Log.d(TAG, "Proximity sensor => NEAR viewState"); lastStateReportIsNear = true; } else { - Log.d(TAG, "Proximity sensor => FAR state"); + Log.d(TAG, "Proximity sensor => FAR viewState"); lastStateReportIsNear = false; } - // Report about new state to listening client. Client can then call - // sensorReportsNearState() to query the current state (NEAR or FAR). + // Report about new viewState to listening client. Client can then call + // sensorReportsNearState() to query the current viewState (NEAR or FAR). if (onSensorStateListener != null) { onSensorStateListener.run(); } diff --git a/app/src/main/res/layout/controller_browser.xml b/app/src/main/res/layout/controller_browser.xml index 09a5f89a7..611c03bca 100644 --- a/app/src/main/res/layout/controller_browser.xml +++ b/app/src/main/res/layout/controller_browser.xml @@ -27,7 +27,7 @@ android:orientation="vertical"> + android:layout_alignTop="@id/recyclerView" + android:layout_alignBottom="@id/recyclerView" /> \ No newline at end of file diff --git a/app/src/main/res/layout/controller_call_menu.xml b/app/src/main/res/layout/controller_call_menu.xml index 21c711e62..14b7c8a02 100644 --- a/app/src/main/res/layout/controller_call_menu.xml +++ b/app/src/main/res/layout/controller_call_menu.xml @@ -25,7 +25,7 @@ android:background="@color/nc_white_color"> diff --git a/app/src/main/res/layout/controller_conversation_info.xml b/app/src/main/res/layout/controller_conversation_info.xml index eb16e45ea..f544b65c0 100644 --- a/app/src/main/res/layout/controller_conversation_info.xml +++ b/app/src/main/res/layout/controller_conversation_info.xml @@ -106,7 +106,7 @@ tools:ignore="UnknownIdInLayout"> diff --git a/app/src/main/res/layout/controller_conversations_rv.xml b/app/src/main/res/layout/controller_conversations_rv.xml index 869ce232e..7e9ab6795 100644 --- a/app/src/main/res/layout/controller_conversations_rv.xml +++ b/app/src/main/res/layout/controller_conversations_rv.xml @@ -75,7 +75,7 @@ android:layout_height="match_parent"> @@ -90,6 +90,7 @@ android:layout_height="wrap_content" android:layout_gravity="bottom|end" android:layout_margin="16dp" + android:visibility="gone" app:srcCompat="@drawable/ic_add_white_24px" /> diff --git a/app/src/main/res/layout/controller_generic_rv.xml b/app/src/main/res/layout/controller_generic_rv.xml index 7bd0ad51e..f1a061abf 100644 --- a/app/src/main/res/layout/controller_generic_rv.xml +++ b/app/src/main/res/layout/controller_generic_rv.xml @@ -34,7 +34,7 @@ android:layout_height="match_parent"> diff --git a/app/src/main/res/menu/menu_conversation_plus_filter.xml b/app/src/main/res/menu/menu_conversation_plus_filter.xml index a1924e010..bbf3d8d9e 100644 --- a/app/src/main/res/menu/menu_conversation_plus_filter.xml +++ b/app/src/main/res/menu/menu_conversation_plus_filter.xml @@ -27,6 +27,7 @@ android:title="@string/nc_search" android:icon="@drawable/ic_search_white_24dp" app:showAsAction="collapseActionView|always" + android:visible="false" android:animateLayoutChanges="true" app:actionViewClass="androidx.appcompat.widget.SearchView" /> diff --git a/build.gradle b/build.gradle index 1dfd6f438..56026dba1 100644 --- a/build.gradle +++ b/build.gradle @@ -22,7 +22,7 @@ buildscript { ext { - kotlinVersion = '1.3.50' + kotlin_version = '1.3.50' } repositories { @@ -34,7 +34,7 @@ buildscript { } dependencies { 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" // NOTE: Do not place your application dependencies here; they belong