From 91e62d9de225225b4b85160e0da506768a7c3852 Mon Sep 17 00:00:00 2001 From: Mario Danic Date: Fri, 11 Oct 2019 16:33:00 +0200 Subject: [PATCH] Koin implementation & EventBus cleanup Signed-off-by: Mario Danic --- app/build.gradle | 12 + .../application/NextcloudTalkApplication.kt | 15 +- .../controllers/BrowserController.java | 2 +- .../AccountVerificationController.java | 23 +- .../talk/controllers/CallController.java | 14 - .../CallNotificationController.java | 17 +- .../talk/controllers/ChatController.kt | 2451 +++++++++-------- .../talk/controllers/ContactsController.java | 17 +- .../controllers/ConversationInfoController.kt | 10 - .../ConversationsListController.java | 11 +- .../RingtoneSelectionController.java | 2 +- .../ServerSelectionController.java | 2 +- .../talk/controllers/SettingsController.java | 6 +- .../controllers/SwitchAccountController.java | 4 +- .../controllers/WebViewLoginController.java | 2 +- .../talk/controllers/base/BaseController.java | 166 -- .../talk/controllers/base/BaseController.kt | 170 ++ .../talk/dagger/modules/RestModule.java | 6 +- .../talk/newarch/data/model/ErrorModel.kt | 89 + .../repository/NextcloudTalkRepositoryImpl.kt | 34 + .../data/source/remote/ApiErrorHandler.kt | 83 + .../newarch/data/source/remote/ApiService.kt | 39 + .../newarch/di/module/CommunicationModule.kt | 32 + .../talk/newarch/di/module/NetworkModule.kt | 250 ++ .../talk/newarch/di/module/StorageModule.kt | 62 + .../repository/NextcloudTalkRepository.kt | 28 + .../usecases/GetConversationsUseCase.kt | 36 + .../domain/usecases/base/BaseUseCase.kt | 51 + .../domain/usecases/base/UseCaseResponse.kt | 29 + .../talk/newarch/utils/NetworkUtils.kt | 118 + .../talk/utils/database/user/UserUtils.java | 2 +- 31 files changed, 2381 insertions(+), 1402 deletions(-) delete mode 100644 app/src/main/java/com/nextcloud/talk/controllers/base/BaseController.java create mode 100644 app/src/main/java/com/nextcloud/talk/controllers/base/BaseController.kt create mode 100644 app/src/main/java/com/nextcloud/talk/newarch/data/model/ErrorModel.kt create mode 100644 app/src/main/java/com/nextcloud/talk/newarch/data/repository/NextcloudTalkRepositoryImpl.kt create mode 100644 app/src/main/java/com/nextcloud/talk/newarch/data/source/remote/ApiErrorHandler.kt create mode 100644 app/src/main/java/com/nextcloud/talk/newarch/data/source/remote/ApiService.kt create mode 100644 app/src/main/java/com/nextcloud/talk/newarch/di/module/CommunicationModule.kt create mode 100644 app/src/main/java/com/nextcloud/talk/newarch/di/module/NetworkModule.kt create mode 100644 app/src/main/java/com/nextcloud/talk/newarch/di/module/StorageModule.kt create mode 100644 app/src/main/java/com/nextcloud/talk/newarch/domain/repository/NextcloudTalkRepository.kt create mode 100644 app/src/main/java/com/nextcloud/talk/newarch/domain/usecases/GetConversationsUseCase.kt create mode 100644 app/src/main/java/com/nextcloud/talk/newarch/domain/usecases/base/BaseUseCase.kt create mode 100644 app/src/main/java/com/nextcloud/talk/newarch/domain/usecases/base/UseCaseResponse.kt create mode 100644 app/src/main/java/com/nextcloud/talk/newarch/utils/NetworkUtils.kt diff --git a/app/build.gradle b/app/build.gradle index 14d7e8454..4b69bf2cd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -137,6 +137,7 @@ android { ext { workVersion = "1.0.1" + koin_version = "2.0.1" } @@ -150,6 +151,17 @@ configurations.all { dependencies { implementation fileTree(include: ['*'], dir: 'libs') + // Koin for Android + implementation "org.koin:koin-android:$koin_version" + // Koin AndroidX Scope features + implementation "org.koin:koin-androidx-scope:$koin_version" + // Koin AndroidX ViewModel features + implementation "org.koin:koin-androidx-viewmodel:$koin_version" + // Koin AndroidX Experimental features + implementation "org.koin:koin-androidx-ext:$koin_version" + + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2' + implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'com.google.android.material:material:1.1.0-beta01' implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta2' 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 6d0f48a1b..82ddd5208 100644 --- a/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt +++ b/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt @@ -20,6 +20,7 @@ */ package com.nextcloud.talk.application +import android.app.Application import android.content.Context import android.os.Build import android.util.Log @@ -48,6 +49,9 @@ import com.nextcloud.talk.jobs.AccountRemovalWorker import com.nextcloud.talk.jobs.CapabilitiesWorker import com.nextcloud.talk.jobs.PushRegistrationWorker 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.utils.ClosedInterfaceImpl import com.nextcloud.talk.utils.DeviceUtils import com.nextcloud.talk.utils.DisplayUtils @@ -62,6 +66,9 @@ import de.cotech.hw.SecurityKeyManager import de.cotech.hw.SecurityKeyManagerConfig import okhttp3.OkHttpClient import org.conscrypt.Conscrypt +import org.koin.android.ext.koin.androidContext +import org.koin.android.ext.koin.androidLogger +import org.koin.core.context.startKoin import org.webrtc.PeerConnectionFactory import org.webrtc.voiceengine.WebRtcAudioManager import org.webrtc.voiceengine.WebRtcAudioUtils @@ -73,7 +80,7 @@ import javax.inject.Singleton @AutoComponent(modules = [BusModule::class, ContextModule::class, DatabaseModule::class, RestModule::class, UserModule::class, ArbitraryStorageModule::class]) @Singleton @AutoInjector(NextcloudTalkApplication::class) -class NextcloudTalkApplication : MultiDexApplication(), LifecycleObserver { +class NextcloudTalkApplication : Application(), LifecycleObserver { //region Fields (components) lateinit var componentApplication: NextcloudTalkApplicationComponent private set @@ -182,6 +189,12 @@ class NextcloudTalkApplication : MultiDexApplication(), LifecycleObserver { .userModule(UserModule()) .arbitraryStorageModule(ArbitraryStorageModule()) .build() + + startKoin { + androidContext(this@NextcloudTalkApplication) + androidLogger() + modules(listOf(CommunicationModule, StorageModule, NetworkModule)) + } } override fun attachBaseContext(base: Context) { 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 7dc655576..a9fdcc729 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 @@ -196,7 +196,7 @@ public class BrowserController extends BaseController implements ListingInterfac } @Override - protected String getTitle() { + public String getTitle() { return currentPath; } 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 22c77de8d..98edbfda3 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/AccountVerificationController.java +++ b/app/src/main/java/com/nextcloud/talk/controllers/AccountVerificationController.java @@ -84,9 +84,6 @@ public class AccountVerificationController extends BaseController { @Inject AppPreferences appPreferences; - @Inject - EventBus eventBus; - @BindView(R.id.progress_text) TextView progressText; @@ -113,18 +110,6 @@ public class AccountVerificationController extends BaseController { } } - @Override - protected void onAttach(@NonNull View view) { - super.onAttach(view); - eventBus.register(this); - } - - @Override - protected void onDetach(@NonNull View view) { - super.onDetach(view); - eventBus.unregister(this); - } - @Override protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) { return inflater.inflate(R.layout.controller_account_verification, container, false); @@ -174,7 +159,7 @@ public class AccountVerificationController extends BaseController { ncApi.getServerStatus(queryUrl) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .as(AutoDispose.autoDisposable(scopeProvider)) + .as(AutoDispose.autoDisposable(getScopeProvider())) .subscribe(new Observer() { @Override public void onSubscribe(Disposable d) { @@ -217,7 +202,7 @@ public class AccountVerificationController extends BaseController { private void findServerTalkApp(String credentials) { ncApi.getRooms(credentials, ApiUtils.getUrlForGetRooms(baseUrl)) .subscribeOn(Schedulers.io()) - .as(AutoDispose.autoDisposable(scopeProvider)) + .as(AutoDispose.autoDisposable(getScopeProvider())) .subscribe(new Observer() { @Override public void onSubscribe(Disposable d) { @@ -254,7 +239,7 @@ public class AccountVerificationController extends BaseController { userId, null, null, appPreferences.getTemporaryClientCertAlias(), null) .subscribeOn(Schedulers.io()) - .as(AutoDispose.autoDisposable(scopeProvider)) + .as(AutoDispose.autoDisposable(getScopeProvider())) .subscribe(new Observer() { @Override public void onSubscribe(Disposable d) { @@ -294,7 +279,7 @@ public class AccountVerificationController extends BaseController { ncApi.getUserProfile(credentials, ApiUtils.getUrlForUserProfile(baseUrl)) .subscribeOn(Schedulers.io()) - .as(AutoDispose.autoDisposable(scopeProvider)) + .as(AutoDispose.autoDisposable(getScopeProvider())) .subscribe(new Observer() { @Override public void onSubscribe(Disposable d) { 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 e63c8fed4..a12318932 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/CallController.java +++ b/app/src/main/java/com/nextcloud/talk/controllers/CallController.java @@ -192,8 +192,6 @@ public class CallController extends BaseController { @Inject NcApi ncApi; @Inject - EventBus eventBus; - @Inject UserUtils userUtils; @Inject AppPreferences appPreferences; @@ -2259,18 +2257,6 @@ public class CallController extends BaseController { } } - @Override - protected void onAttach(@NonNull View view) { - super.onAttach(view); - eventBus.register(this); - } - - @Override - protected void onDetach(@NonNull View view) { - super.onDetach(view); - eventBus.unregister(this); - } - private class MicrophoneButtonTouchListener implements View.OnTouchListener { @SuppressLint("ClickableViewAccessibility") 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 78d0e440b..4e9e1243a 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/CallNotificationController.java +++ b/app/src/main/java/com/nextcloud/talk/controllers/CallNotificationController.java @@ -200,7 +200,7 @@ public class CallNotificationController extends BaseController { .subscribeOn(Schedulers.io()) .takeWhile(observable -> !leavingScreen) .retry(3) - .as(AutoDispose.autoDisposable(scopeProvider)) + .as(AutoDispose.autoDisposable(getScopeProvider())) .subscribe(new Observer() { @Override public void onSubscribe(Disposable d) { @@ -249,7 +249,7 @@ public class CallNotificationController extends BaseController { .subscribeOn(Schedulers.io()) .retry(3) .observeOn(AndroidSchedulers.mainThread()) - .as(AutoDispose.autoDisposable(scopeProvider)) + .as(AutoDispose.autoDisposable(getScopeProvider())) .subscribe(new Observer() { @Override public void onSubscribe(Disposable d) { @@ -390,19 +390,6 @@ public class CallNotificationController extends BaseController { avatarImageView.setLayoutParams(layoutParams); } - @Override - protected void onDetach(@NonNull View view) { - super.onDetach(view); - eventBus.unregister(this); - } - - @Override - protected void onAttach(@NonNull View view) { - super.onAttach(view); - eventBus.register(this); - - } - private void loadAvatar() { switch (currentConversation.getType()) { case ROOM_TYPE_ONE_TO_ONE_CALL: 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 62f09b3af..b2b2c93a5 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt @@ -20,7 +20,6 @@ package com.nextcloud.talk.controllers - import android.content.Context import android.content.Intent import android.content.res.Resources @@ -63,11 +62,6 @@ 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.models.json.conversations.Conversation -import com.nextcloud.talk.models.json.conversations.RoomOverall -import com.nextcloud.talk.models.json.conversations.RoomsOverall -import com.nextcloud.talk.models.json.generic.GenericOverall -import com.nextcloud.talk.models.json.mention.Mention import com.nextcloud.talk.R import com.nextcloud.talk.activities.MagicCallActivity import com.nextcloud.talk.adapters.messages.MagicIncomingTextMessageViewHolder @@ -85,6 +79,11 @@ import com.nextcloud.talk.events.WebSocketCommunicationEvent import com.nextcloud.talk.models.database.UserEntity import com.nextcloud.talk.models.json.chat.ChatMessage import com.nextcloud.talk.models.json.chat.ChatOverall +import com.nextcloud.talk.models.json.conversations.Conversation +import com.nextcloud.talk.models.json.conversations.RoomOverall +import com.nextcloud.talk.models.json.conversations.RoomsOverall +import com.nextcloud.talk.models.json.generic.GenericOverall +import com.nextcloud.talk.models.json.mention.Mention import com.nextcloud.talk.presenters.MentionAutocompletePresenter import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.ConductorRemapping @@ -95,7 +94,6 @@ import com.nextcloud.talk.utils.MagicCharPolicy import com.nextcloud.talk.utils.NotificationUtils import com.nextcloud.talk.utils.bundle.BundleKeys import com.nextcloud.talk.utils.database.user.UserUtils -import com.nextcloud.talk.utils.preferences.AppPreferences import com.nextcloud.talk.utils.singletons.ApplicationWideCurrentRoomHolder import com.nextcloud.talk.utils.text.Spans import com.nextcloud.talk.webrtc.MagicWebSocketInstance @@ -115,7 +113,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 org.parceler.Parcels @@ -132,1219 +129,1385 @@ class ChatController(args: Bundle) : BaseController(), MessagesListAdapter .OnLoadMoreListener, MessagesListAdapter.Formatter, MessagesListAdapter .OnMessageLongClickListener, MessageHolders.ContentChecker { - @Inject - @JvmField - var ncApi: NcApi? = null - @Inject - @JvmField - var userUtils: UserUtils? = null - @Inject - @JvmField - var appPreferences: AppPreferences? = null - @Inject - @JvmField - var context: Context? = null - @Inject - @JvmField - var eventBus: EventBus? = null - @BindView(R.id.messagesListView) - @JvmField - var messagesListView: MessagesList? = null - @BindView(R.id.messageInputView) - @JvmField - var messageInputView: MessageInput? = null - @BindView(R.id.messageInput) - @JvmField - var messageInput: EmojiEditText? = null - @BindView(R.id.popupBubbleView) - @JvmField - var popupBubble: PopupBubble? = null - @BindView(R.id.progressBar) - @JvmField - var loadingProgressBar: ProgressBar? = null - @BindView(R.id.smileyButton) - @JvmField - var smileyButton: ImageButton? = null - @BindView(R.id.lobby_view) - @JvmField - var lobbyView: RelativeLayout? = null - @BindView(R.id.lobby_text_view) - @JvmField - var conversationLobbyText: TextView? = null - var roomToken: String? = null - val conversationUser: UserEntity? - val roomPassword: String - var credentials: String? = null - var currentConversation: Conversation? = null - var inConversation = false - var historyRead = false - var globalLastKnownFutureMessageId = -1 - var globalLastKnownPastMessageId = -1 - var adapter: MessagesListAdapter? = null - var mentionAutocomplete: Autocomplete<*>? = null - var layoutManager: LinearLayoutManager? = null - var lookingIntoFuture = false - var newMessagesCount = 0 - var startCallFromNotification: Boolean? = null - val roomId: String - val voiceOnly: Boolean - var isFirstMessagesProcessing = true - var isLeavingForConversation: Boolean = false - var isLinkPreviewAllowed: Boolean = false - var wasDetached: Boolean = false - var emojiPopup: EmojiPopup? = null + @Inject + @JvmField + var ncApi: NcApi? = null + @Inject + @JvmField + var userUtils: UserUtils? = null + @BindView(R.id.messagesListView) + @JvmField + var messagesListView: MessagesList? = null + @BindView(R.id.messageInputView) + @JvmField + var messageInputView: MessageInput? = null + @BindView(R.id.messageInput) + @JvmField + var messageInput: EmojiEditText? = null + @BindView(R.id.popupBubbleView) + @JvmField + var popupBubble: PopupBubble? = null + @BindView(R.id.progressBar) + @JvmField + var loadingProgressBar: ProgressBar? = null + @BindView(R.id.smileyButton) + @JvmField + var smileyButton: ImageButton? = null + @BindView(R.id.lobby_view) + @JvmField + var lobbyView: RelativeLayout? = null + @BindView(R.id.lobby_text_view) + @JvmField + var conversationLobbyText: TextView? = null + var roomToken: String? = null + val conversationUser: UserEntity? + val roomPassword: String + var credentials: String? = null + var currentConversation: Conversation? = null + var inConversation = false + var historyRead = false + var globalLastKnownFutureMessageId = -1 + var globalLastKnownPastMessageId = -1 + var adapter: MessagesListAdapter? = null + var mentionAutocomplete: Autocomplete<*>? = null + var layoutManager: LinearLayoutManager? = null + var lookingIntoFuture = false + var newMessagesCount = 0 + var startCallFromNotification: Boolean? = null + val roomId: String + val voiceOnly: Boolean + var isFirstMessagesProcessing = true + var isLeavingForConversation: Boolean = false + var isLinkPreviewAllowed: Boolean = false + var wasDetached: Boolean = false + var emojiPopup: EmojiPopup? = null - var myFirstMessage: CharSequence? = null - var checkingLobbyStatus: Boolean = false + var myFirstMessage: CharSequence? = null + var checkingLobbyStatus: Boolean = false - var conversationInfoMenuItem: MenuItem? = null - var conversationVoiceCallMenuItem: MenuItem? = null - var conversationVideoMenuItem: MenuItem? = null + var conversationInfoMenuItem: MenuItem? = null + var conversationVoiceCallMenuItem: MenuItem? = null + var conversationVideoMenuItem: MenuItem? = null - var magicWebSocketInstance: MagicWebSocketInstance? = null + var magicWebSocketInstance: MagicWebSocketInstance? = null - var lobbyTimerHandler: Handler? = null - val roomJoined: Boolean = false + var lobbyTimerHandler: Handler? = null + val roomJoined: Boolean = false - init { - setHasOptionsMenu(true) - NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + init { + setHasOptionsMenu(true) + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) - this.conversationUser = args.getParcelable(BundleKeys.KEY_USER_ENTITY) - this.roomId = args.getString(BundleKeys.KEY_ROOM_ID, "") - this.roomToken = args.getString(BundleKeys.KEY_ROOM_TOKEN, "") + this.conversationUser = args.getParcelable(BundleKeys.KEY_USER_ENTITY) + this.roomId = args.getString(BundleKeys.KEY_ROOM_ID, "") + this.roomToken = args.getString(BundleKeys.KEY_ROOM_TOKEN, "") - if (args.containsKey(BundleKeys.KEY_ACTIVE_CONVERSATION)) { - this.currentConversation = Parcels.unwrap(args.getParcelable(BundleKeys.KEY_ACTIVE_CONVERSATION)) - } - - this.roomPassword = args.getString(BundleKeys.KEY_CONVERSATION_PASSWORD, "") - - if (conversationUser?.userId == "?") { - credentials = null - } else { - credentials = ApiUtils.getCredentials(conversationUser?.username, conversationUser?.token) - } - - if (args.containsKey(BundleKeys.KEY_FROM_NOTIFICATION_START_CALL)) { - this.startCallFromNotification = args.getBoolean(BundleKeys.KEY_FROM_NOTIFICATION_START_CALL) - } - - this.voiceOnly = args.getBoolean(BundleKeys.KEY_CALL_VOICE_ONLY, false) + if (args.containsKey(BundleKeys.KEY_ACTIVE_CONVERSATION)) { + this.currentConversation = Parcels.unwrap( + args.getParcelable(BundleKeys.KEY_ACTIVE_CONVERSATION) + ) } - private fun getRoomInfo() { - val shouldRepeat = conversationUser?.hasSpreedFeatureCapability("webinary-lobby") ?: false - if (shouldRepeat) { - checkingLobbyStatus = true - } + this.roomPassword = args.getString(BundleKeys.KEY_CONVERSATION_PASSWORD, "") - - if (conversationUser != null) { - ncApi?.getRoom(credentials, ApiUtils.getRoom(conversationUser.baseUrl, roomToken))?.subscribeOn(Schedulers.io()) - ?.observeOn(AndroidSchedulers.mainThread()) - ?.`as`(AutoDispose.autoDisposable(scopeProvider)) - ?.subscribe(object : Observer { - override fun onSubscribe(d: Disposable) { - } - - override fun onNext(roomOverall: RoomOverall) { - currentConversation = roomOverall.ocs.data - loadAvatarForStatusBar() - - setTitle() - setupMentionAutocomplete() - checkReadOnlyState() - checkLobbyState() - - if (!inConversation) { - joinRoomWithPassword() - } - - } - - override fun onError(e: Throwable) { - - } - - override fun onComplete() { - if (shouldRepeat) { - if (lobbyTimerHandler == null) { - lobbyTimerHandler = Handler() - } - - lobbyTimerHandler?.postDelayed({ getRoomInfo() }, 5000) - } - } - }) - } + if (conversationUser?.userId == "?") { + credentials = null + } else { + credentials = ApiUtils.getCredentials(conversationUser?.username, conversationUser?.token) } - private fun handleFromNotification() { - ncApi?.getRooms(credentials, ApiUtils.getUrlForGetRooms(conversationUser?.baseUrl)) - ?.subscribeOn(Schedulers.io())?.observeOn(AndroidSchedulers.mainThread()) - ?.`as`(AutoDispose.autoDisposable(scopeProvider)) - ?.subscribe(object : Observer { - override fun onSubscribe(d: Disposable) { - } - - override fun onNext(roomsOverall: RoomsOverall) { - for (conversation in roomsOverall.ocs.data) { - if (roomId == conversation.roomId) { - roomToken = conversation.token - currentConversation = conversation - setTitle() - getRoomInfo() - break - } - } - } - - override fun onError(e: Throwable) { - - } - - override fun onComplete() { - - } - }) + if (args.containsKey(BundleKeys.KEY_FROM_NOTIFICATION_START_CALL)) { + this.startCallFromNotification = args.getBoolean(BundleKeys.KEY_FROM_NOTIFICATION_START_CALL) } - override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { - return inflater.inflate(R.layout.controller_chat, container, false) + this.voiceOnly = args.getBoolean(BundleKeys.KEY_CALL_VOICE_ONLY, false) + } + + private fun getRoomInfo() { + val shouldRepeat = conversationUser?.hasSpreedFeatureCapability("webinary-lobby") ?: false + if (shouldRepeat) { + checkingLobbyStatus = true } - private fun loadAvatarForStatusBar() { - if (currentConversation != null && currentConversation?.type != null && - currentConversation?.type == Conversation.ConversationType - .ROOM_TYPE_ONE_TO_ONE_CALL && activity != null && conversationVoiceCallMenuItem != null) { - val avatarSize = DisplayUtils.convertDpToPixel(conversationVoiceCallMenuItem?.icon!! - .intrinsicWidth.toFloat(), activity).toInt() - val imageRequest = DisplayUtils.getImageRequestForUrl(ApiUtils.getUrlForAvatarWithNameAndPixels(conversationUser?.baseUrl, - currentConversation?.name, avatarSize / 2), null) - - val imagePipeline = Fresco.getImagePipeline() - val dataSource = imagePipeline.fetchDecodedImage(imageRequest, null) - - dataSource.subscribe(object : BaseBitmapDataSubscriber() { - override fun onNewResultImpl(bitmap: Bitmap?) { - if (actionBar != null && bitmap != null && resources != null) { - val roundedBitmapDrawable = RoundedBitmapDrawableFactory.create(resources!!, bitmap) - roundedBitmapDrawable.isCircular = true - roundedBitmapDrawable.setAntiAlias(true) - actionBar?.setIcon(roundedBitmapDrawable) - } - } - - override fun onFailureImpl(dataSource: DataSource>) {} - }, UiThreadImmediateExecutorService.getInstance()) - } - } - - override fun onViewBound(view: View) { - super.onViewBound(view) - - actionBar?.show() - var adapterWasNull = false - - if (adapter == null) { - loadingProgressBar?.visibility = View.VISIBLE - - adapterWasNull = true - - val messageHolders = MessageHolders() - messageHolders.setIncomingTextConfig(MagicIncomingTextMessageViewHolder::class.java, R.layout.item_custom_incoming_text_message) - messageHolders.setOutcomingTextConfig(MagicOutcomingTextMessageViewHolder::class.java, R.layout.item_custom_outcoming_text_message) - - messageHolders.setIncomingImageConfig(MagicPreviewMessageViewHolder::class.java, R.layout.item_custom_incoming_preview_message) - messageHolders.setOutcomingImageConfig(MagicPreviewMessageViewHolder::class.java, R.layout.item_custom_outcoming_preview_message) - - messageHolders.registerContentType(CONTENT_TYPE_SYSTEM_MESSAGE, MagicSystemMessageViewHolder::class.java, - R.layout.item_system_message, MagicSystemMessageViewHolder::class.java, R.layout.item_system_message, - this) - - messageHolders.registerContentType(CONTENT_TYPE_UNREAD_NOTICE_MESSAGE, - MagicUnreadNoticeMessageViewHolder::class.java, R.layout.item_date_header, - MagicUnreadNoticeMessageViewHolder::class.java, R.layout.item_date_header, this) - - adapter = MessagesListAdapter(conversationUser?.userId, messageHolders, ImageLoader { imageView, url, payload -> - val draweeController = Fresco.newDraweeControllerBuilder() - .setImageRequest(DisplayUtils.getImageRequestForUrl(url, conversationUser)) - .setControllerListener(DisplayUtils.getImageControllerListener(imageView)) - .setOldController(imageView.controller) - .setAutoPlayAnimations(true) - .build() - imageView.controller = draweeController - }) - } else { - messagesListView?.visibility = View.VISIBLE - } - - messagesListView?.setAdapter(adapter) - adapter?.setLoadMoreListener(this) - adapter?.setDateHeadersFormatter { format(it) } - adapter?.setOnMessageLongClickListener { onMessageLongClick(it) } - - layoutManager = messagesListView?.layoutManager as LinearLayoutManager? - - popupBubble?.setRecyclerView(messagesListView) - - popupBubble?.setPopupBubbleListener { context -> - if (newMessagesCount != 0) { - val scrollPosition: Int - if (newMessagesCount - 1 < 0) { - scrollPosition = 0 - } else { - scrollPosition = newMessagesCount - 1 - } - Handler().postDelayed({ messagesListView?.smoothScrollToPosition(scrollPosition) }, 200) + if (conversationUser != null) { + ncApi?.getRoom(credentials, ApiUtils.getRoom(conversationUser.baseUrl, roomToken)) + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.`as`(AutoDispose.autoDisposable(scopeProvider)) + ?.subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { } - } - messagesListView?.addOnScrollListener(object : RecyclerView.OnScrollListener() { - override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { - super.onScrollStateChanged(recyclerView, newState) + override fun onNext(roomOverall: RoomOverall) { + currentConversation = roomOverall.ocs.data + loadAvatarForStatusBar() - if (newState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) { - if (newMessagesCount != 0 && layoutManager != null) { - if (layoutManager!!.findFirstCompletelyVisibleItemPosition() < - newMessagesCount) { - newMessagesCount = 0 + setTitle() + setupMentionAutocomplete() + checkReadOnlyState() + checkLobbyState() - if (popupBubble != null && popupBubble!!.isShown) { - popupBubble?.hide() - } - } - } - } - } - }) - - - val filters = arrayOfNulls(1) - val lengthFilter = conversationUser?.messageMaxLength ?: 1000 - - - filters[0] = InputFilter.LengthFilter(lengthFilter) - messageInput?.filters = filters - - messageInput?.addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { + if (!inConversation) { + joinRoomWithPassword() + } } - override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { - if (s.length >= lengthFilter) { - messageInput?.error = String.format(Objects.requireNonNull - (resources).getString(R.string.nc_limit_hit), Integer.toString(lengthFilter)) - } else { - messageInput?.error = null - } - - val editable = messageInput?.editableText - if (editable != null && messageInput != null) { - val mentionSpans = editable.getSpans(0, messageInput!!.length(), - Spans.MentionChipSpan::class.java) - var mentionSpan: Spans.MentionChipSpan - for (i in mentionSpans.indices) { - mentionSpan = mentionSpans[i] - if (start >= editable.getSpanStart(mentionSpan) && start < editable.getSpanEnd(mentionSpan)) { - if (editable.subSequence(editable.getSpanStart(mentionSpan), - editable.getSpanEnd(mentionSpan)).toString().trim { it <= ' ' } != mentionSpan.label) { - editable.removeSpan(mentionSpan) - } - } - } - } - } - - override fun afterTextChanged(s: Editable) { + override fun onError(e: Throwable) { } - }) - messageInputView?.setAttachmentsListener { - showBrowserScreen(BrowserController - .BrowserType.DAV_BROWSER) - } + override fun onComplete() { + if (shouldRepeat) { + if (lobbyTimerHandler == null) { + lobbyTimerHandler = Handler() + } - messageInputView?.button?.setOnClickListener { v -> submitMessage() } + lobbyTimerHandler?.postDelayed({ getRoomInfo() }, 5000) + } + } + }) + } + } - messageInputView?.button?.contentDescription = resources?.getString(R.string - .nc_description_send_message_button) + private fun handleFromNotification() { + ncApi?.getRooms(credentials, ApiUtils.getUrlForGetRooms(conversationUser?.baseUrl)) + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.`as`(AutoDispose.autoDisposable(scopeProvider)) + ?.subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + } - if (currentConversation != null && currentConversation?.roomId != null) { - loadAvatarForStatusBar() - checkLobbyState() - setTitle() - } - - if (adapterWasNull) { - // we're starting - if (TextUtils.isEmpty(roomToken)) { - handleFromNotification() - } else { + override fun onNext(roomsOverall: RoomsOverall) { + for (conversation in roomsOverall.ocs.data) { + if (roomId == conversation.roomId) { + roomToken = conversation.token + currentConversation = conversation + setTitle() getRoomInfo() + break + } } + } + + override fun onError(e: Throwable) { + + } + + override fun onComplete() { + + } + }) + } + + override fun inflateView( + inflater: LayoutInflater, + container: ViewGroup + ): View { + return inflater.inflate(R.layout.controller_chat, container, false) + } + + private fun loadAvatarForStatusBar() { + if (currentConversation != null && currentConversation?.type != null && + currentConversation?.type == Conversation.ConversationType + .ROOM_TYPE_ONE_TO_ONE_CALL && activity != null && conversationVoiceCallMenuItem != null + ) { + val avatarSize = DisplayUtils.convertDpToPixel( + conversationVoiceCallMenuItem?.icon!! + .intrinsicWidth.toFloat(), activity + ) + .toInt() + + val imageRequest = DisplayUtils.getImageRequestForUrl( + ApiUtils.getUrlForAvatarWithNameAndPixels( + conversationUser?.baseUrl, + currentConversation?.name, avatarSize / 2 + ), null + ) + + val imagePipeline = Fresco.getImagePipeline() + val dataSource = imagePipeline.fetchDecodedImage(imageRequest, null) + + dataSource.subscribe(object : BaseBitmapDataSubscriber() { + override fun onNewResultImpl(bitmap: Bitmap?) { + if (actionBar != null && bitmap != null && resources != null) { + val roundedBitmapDrawable = RoundedBitmapDrawableFactory.create(resources!!, bitmap) + roundedBitmapDrawable.isCircular = true + roundedBitmapDrawable.setAntiAlias(true) + actionBar?.setIcon(roundedBitmapDrawable) + } } + + override fun onFailureImpl(dataSource: DataSource>) {} + }, UiThreadImmediateExecutorService.getInstance()) + } + } + + override fun onViewBound(view: View) { + super.onViewBound(view) + + actionBar?.show() + var adapterWasNull = false + + if (adapter == null) { + loadingProgressBar?.visibility = View.VISIBLE + + adapterWasNull = true + + val messageHolders = MessageHolders() + messageHolders.setIncomingTextConfig( + MagicIncomingTextMessageViewHolder::class.java, R.layout.item_custom_incoming_text_message + ) + messageHolders.setOutcomingTextConfig( + MagicOutcomingTextMessageViewHolder::class.java, + R.layout.item_custom_outcoming_text_message + ) + + messageHolders.setIncomingImageConfig( + MagicPreviewMessageViewHolder::class.java, R.layout.item_custom_incoming_preview_message + ) + messageHolders.setOutcomingImageConfig( + MagicPreviewMessageViewHolder::class.java, R.layout.item_custom_outcoming_preview_message + ) + + messageHolders.registerContentType( + CONTENT_TYPE_SYSTEM_MESSAGE, MagicSystemMessageViewHolder::class.java, + R.layout.item_system_message, MagicSystemMessageViewHolder::class.java, + R.layout.item_system_message, + this + ) + + messageHolders.registerContentType( + CONTENT_TYPE_UNREAD_NOTICE_MESSAGE, + MagicUnreadNoticeMessageViewHolder::class.java, R.layout.item_date_header, + MagicUnreadNoticeMessageViewHolder::class.java, R.layout.item_date_header, this + ) + + adapter = MessagesListAdapter( + conversationUser?.userId, messageHolders, ImageLoader { imageView, url, payload -> + val draweeController = Fresco.newDraweeControllerBuilder() + .setImageRequest(DisplayUtils.getImageRequestForUrl(url, conversationUser)) + .setControllerListener(DisplayUtils.getImageControllerListener(imageView)) + .setOldController(imageView.controller) + .setAutoPlayAnimations(true) + .build() + imageView.controller = draweeController + }) + } else { + messagesListView?.visibility = View.VISIBLE } + messagesListView?.setAdapter(adapter) + adapter?.setLoadMoreListener(this) + adapter?.setDateHeadersFormatter { format(it) } + adapter?.setOnMessageLongClickListener { onMessageLongClick(it) } - private fun checkReadOnlyState() { - if (currentConversation != null) { - if (currentConversation?.shouldShowLobby(conversationUser)?: false || currentConversation?.conversationReadOnlyState != null && currentConversation?.conversationReadOnlyState == Conversation.ConversationReadOnlyState.CONVERSATION_READ_ONLY) { + layoutManager = messagesListView?.layoutManager as LinearLayoutManager? - conversationVoiceCallMenuItem?.icon?.alpha = 99 - conversationVideoMenuItem?.icon?.alpha = 99 - messageInputView?.visibility = View.GONE + popupBubble?.setRecyclerView(messagesListView) - } else { - if (conversationVoiceCallMenuItem != null) { - conversationVoiceCallMenuItem?.icon?.alpha = 255 - } - - if (conversationVideoMenuItem != null) { - conversationVideoMenuItem?.icon?.alpha = 255 - } - - if (currentConversation != null && currentConversation!!.shouldShowLobby - (conversationUser)) { - messageInputView?.visibility = View.GONE - } else { - messageInputView?.visibility = View.VISIBLE - } - } - } - } - - private fun checkLobbyState() { - if (currentConversation != null && currentConversation?.isLobbyViewApplicable(conversationUser) ?: false) { - - if (!checkingLobbyStatus) { - getRoomInfo() - } - - if (currentConversation?.shouldShowLobby(conversationUser) ?: false) { - lobbyView?.visibility = View.VISIBLE - messagesListView?.visibility = View.GONE - messageInputView?.visibility = View.GONE - loadingProgressBar?.visibility = View.GONE - - if (currentConversation?.lobbyTimer != null && currentConversation?.lobbyTimer != - 0L) { - conversationLobbyText?.text = String.format(resources!!.getString(R.string.nc_lobby_waiting_with_date), DateUtils.getLocalDateStringFromTimestampForLobby(currentConversation?.lobbyTimer - ?: 0)) - } else { - conversationLobbyText?.setText(R.string.nc_lobby_waiting) - } - } else { - lobbyView?.visibility = View.GONE - messagesListView?.visibility = View.VISIBLE - messageInput?.visibility = View.VISIBLE - } + popupBubble?.setPopupBubbleListener { context -> + if (newMessagesCount != 0) { + val scrollPosition: Int + if (newMessagesCount - 1 < 0) { + scrollPosition = 0 } else { - lobbyView?.visibility = View.GONE - messagesListView?.visibility = View.VISIBLE - messageInput?.visibility = View.VISIBLE + scrollPosition = newMessagesCount - 1 } + Handler().postDelayed({ messagesListView?.smoothScrollToPosition(scrollPosition) }, 200) + } } - private fun showBrowserScreen(browserType: BrowserController.BrowserType) { - val bundle = Bundle() - bundle.putParcelable(BundleKeys.KEY_BROWSER_TYPE, Parcels.wrap(browserType)) - bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, Parcels.wrap(conversationUser)) - bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomToken) - router.pushController(RouterTransaction.with(BrowserController(bundle)) - .pushChangeHandler(VerticalChangeHandler()) - .popChangeHandler(VerticalChangeHandler())) - } + messagesListView?.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrollStateChanged( + recyclerView: RecyclerView, + newState: Int + ) { + super.onScrollStateChanged(recyclerView, newState) - private fun showConversationInfoScreen() { - val bundle = Bundle() - bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, conversationUser) - bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomToken) - router.pushController(RouterTransaction.with(ConversationInfoController(bundle)) - .pushChangeHandler(HorizontalChangeHandler()) - .popChangeHandler(HorizontalChangeHandler())) - } + if (newState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) { + if (newMessagesCount != 0 && layoutManager != null) { + if (layoutManager!!.findFirstCompletelyVisibleItemPosition() < + newMessagesCount + ) { + newMessagesCount = 0 - private fun setupMentionAutocomplete() { - val elevation = 6f - val backgroundDrawable = ColorDrawable(resources!!.getColor(R.color.bg_default)) - val presenter = MentionAutocompletePresenter(applicationContext, roomToken) - val callback = MentionAutocompleteCallback(activity, - conversationUser, messageInput) - - if (mentionAutocomplete == null && messageInput != null) { - mentionAutocomplete = Autocomplete.on(messageInput) - .with(elevation) - .with(backgroundDrawable) - .with(MagicCharPolicy('@')) - .with(presenter) - .with(callback) - .build() - } - } - - override fun onAttach(view: View) { - super.onAttach(view) - eventBus?.register(this) - - if (conversationUser?.userId != "?" && conversationUser?.hasSpreedFeatureCapability("mention-flag") ?: false && activity != null) { - activity?.findViewById(R.id.toolbar)?.setOnClickListener { v -> - showConversationInfoScreen() + if (popupBubble != null && popupBubble!!.isShown) { + popupBubble?.hide() + } } + } + } + } + }) + + val filters = arrayOfNulls(1) + val lengthFilter = conversationUser?.messageMaxLength ?: 1000 + + + filters[0] = InputFilter.LengthFilter(lengthFilter) + messageInput?.filters = filters + + messageInput?.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged( + s: CharSequence, + start: Int, + count: Int, + after: Int + ) { + + } + + override fun onTextChanged( + s: CharSequence, + start: Int, + before: Int, + count: Int + ) { + if (s.length >= lengthFilter) { + messageInput?.error = String.format( + Objects.requireNonNull + (resources).getString(R.string.nc_limit_hit), Integer.toString(lengthFilter) + ) + } else { + messageInput?.error = null } - isLeavingForConversation = false - ApplicationWideCurrentRoomHolder.getInstance().currentRoomId = roomId - ApplicationWideCurrentRoomHolder.getInstance().currentRoomToken = roomId - ApplicationWideCurrentRoomHolder.getInstance().isInCall = false - ApplicationWideCurrentRoomHolder.getInstance().userInRoom = conversationUser + val editable = messageInput?.editableText + if (editable != null && messageInput != null) { + val mentionSpans = editable.getSpans( + 0, messageInput!!.length(), + Spans.MentionChipSpan::class.java + ) + var mentionSpan: Spans.MentionChipSpan + for (i in mentionSpans.indices) { + mentionSpan = mentionSpans[i] + if (start >= editable.getSpanStart(mentionSpan) && start < editable.getSpanEnd( + mentionSpan + ) + ) { + if (editable.subSequence( + editable.getSpanStart(mentionSpan), + editable.getSpanEnd(mentionSpan) + ).toString().trim { it <= ' ' } != mentionSpan.label + ) { + editable.removeSpan(mentionSpan) + } + } + } + } + } - isLinkPreviewAllowed = appPreferences?.areLinkPreviewsAllowed ?: false + override fun afterTextChanged(s: Editable) { - emojiPopup = messageInput?.let { - EmojiPopup.Builder.fromRootView(view).setOnEmojiPopupShownListener { + } + }) + + messageInputView?.setAttachmentsListener { + showBrowserScreen( + BrowserController + .BrowserType.DAV_BROWSER + ) + } + + messageInputView?.button?.setOnClickListener { v -> submitMessage() } + + messageInputView?.button?.contentDescription = resources?.getString( + R.string + .nc_description_send_message_button + ) + + if (currentConversation != null && currentConversation?.roomId != null) { + loadAvatarForStatusBar() + checkLobbyState() + setTitle() + } + + if (adapterWasNull) { + // we're starting + if (TextUtils.isEmpty(roomToken)) { + handleFromNotification() + } else { + getRoomInfo() + } + } + } + + private fun checkReadOnlyState() { + if (currentConversation != null) { + if (currentConversation?.shouldShowLobby( + conversationUser + ) ?: false || currentConversation?.conversationReadOnlyState != null && currentConversation?.conversationReadOnlyState == Conversation.ConversationReadOnlyState.CONVERSATION_READ_ONLY + ) { + + conversationVoiceCallMenuItem?.icon?.alpha = 99 + conversationVideoMenuItem?.icon?.alpha = 99 + messageInputView?.visibility = View.GONE + + } else { + if (conversationVoiceCallMenuItem != null) { + conversationVoiceCallMenuItem?.icon?.alpha = 255 + } + + if (conversationVideoMenuItem != null) { + conversationVideoMenuItem?.icon?.alpha = 255 + } + + if (currentConversation != null && currentConversation!!.shouldShowLobby + (conversationUser) + ) { + messageInputView?.visibility = View.GONE + } else { + messageInputView?.visibility = View.VISIBLE + } + } + } + } + + private fun checkLobbyState() { + if (currentConversation != null && currentConversation?.isLobbyViewApplicable( + conversationUser + ) ?: false + ) { + + if (!checkingLobbyStatus) { + getRoomInfo() + } + + if (currentConversation?.shouldShowLobby(conversationUser) ?: false) { + lobbyView?.visibility = View.VISIBLE + messagesListView?.visibility = View.GONE + messageInputView?.visibility = View.GONE + loadingProgressBar?.visibility = View.GONE + + if (currentConversation?.lobbyTimer != null && currentConversation?.lobbyTimer != + 0L + ) { + conversationLobbyText?.text = String.format( + resources!!.getString(R.string.nc_lobby_waiting_with_date), + DateUtils.getLocalDateStringFromTimestampForLobby( + currentConversation?.lobbyTimer + ?: 0 + ) + ) + } else { + conversationLobbyText?.setText(R.string.nc_lobby_waiting) + } + } else { + lobbyView?.visibility = View.GONE + messagesListView?.visibility = View.VISIBLE + messageInput?.visibility = View.VISIBLE + } + } else { + lobbyView?.visibility = View.GONE + messagesListView?.visibility = View.VISIBLE + messageInput?.visibility = View.VISIBLE + } + } + + private fun showBrowserScreen(browserType: BrowserController.BrowserType) { + val bundle = Bundle() + bundle.putParcelable( + BundleKeys.KEY_BROWSER_TYPE, Parcels.wrap(browserType) + ) + bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, Parcels.wrap(conversationUser)) + bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomToken) + router.pushController( + RouterTransaction.with(BrowserController(bundle)) + .pushChangeHandler(VerticalChangeHandler()) + .popChangeHandler(VerticalChangeHandler()) + ) + } + + private fun showConversationInfoScreen() { + val bundle = Bundle() + bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, conversationUser) + bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomToken) + router.pushController( + RouterTransaction.with(ConversationInfoController(bundle)) + .pushChangeHandler(HorizontalChangeHandler()) + .popChangeHandler(HorizontalChangeHandler()) + ) + } + + private fun setupMentionAutocomplete() { + val elevation = 6f + val backgroundDrawable = ColorDrawable(resources!!.getColor(R.color.bg_default)) + val presenter = MentionAutocompletePresenter(applicationContext, roomToken) + val callback = MentionAutocompleteCallback( + activity, + conversationUser, messageInput + ) + + if (mentionAutocomplete == null && messageInput != null) { + mentionAutocomplete = Autocomplete.on(messageInput) + .with(elevation) + .with(backgroundDrawable) + .with(MagicCharPolicy('@')) + .with(presenter) + .with(callback) + .build() + } + } + + override fun onAttach(view: View) { + super.onAttach(view) + + if (conversationUser?.userId != "?" && conversationUser?.hasSpreedFeatureCapability( + "mention-flag" + ) ?: false && activity != null + ) { + activity?.findViewById(R.id.toolbar) + ?.setOnClickListener { v -> + showConversationInfoScreen() + } + } + + isLeavingForConversation = false + ApplicationWideCurrentRoomHolder.getInstance() + .currentRoomId = roomId + ApplicationWideCurrentRoomHolder.getInstance() + .currentRoomToken = roomId + ApplicationWideCurrentRoomHolder.getInstance() + .isInCall = false + ApplicationWideCurrentRoomHolder.getInstance() + .userInRoom = conversationUser + + isLinkPreviewAllowed = appPreferences?.areLinkPreviewsAllowed ?: false + + emojiPopup = messageInput?.let { + EmojiPopup.Builder.fromRootView(view) + .setOnEmojiPopupShownListener { if (resources != null) { - smileyButton?.setColorFilter(resources!!.getColor(R.color.colorPrimary), PorterDuff.Mode.SRC_IN) + smileyButton?.setColorFilter( + resources!!.getColor(R.color.colorPrimary), PorterDuff.Mode.SRC_IN + ) } - }.setOnEmojiPopupDismissListener { - smileyButton?.setColorFilter(resources!!.getColor(R.color.emoji_icons), - PorterDuff.Mode.SRC_IN) - }.setOnEmojiClickListener { emoji, imageView -> messageInput?.editableText?.append(" ") }.build(it) - } + } + .setOnEmojiPopupDismissListener { + smileyButton?.setColorFilter( + resources!!.getColor(R.color.emoji_icons), + PorterDuff.Mode.SRC_IN + ) + } + .setOnEmojiClickListener { emoji, imageView -> messageInput?.editableText?.append(" ") } + .build(it) + } - if (activity != null) { - KeyboardUtils(activity, getView(), false) - } + if (activity != null) { + KeyboardUtils(activity, getView(), false) + } + cancelNotificationsForCurrentConversation() + + if (inConversation) { + if (wasDetached && conversationUser?.hasSpreedFeatureCapability("no-ping") ?: false) { + currentConversation?.sessionId = "0" + wasDetached = false + joinRoomWithPassword() + } + } + } + + private fun cancelNotificationsForCurrentConversation() { + if (conversationUser != null) { + if (!conversationUser.hasSpreedFeatureCapability("no-ping") && !TextUtils.isEmpty(roomId)) { + NotificationUtils.cancelExistingNotificationsForRoom( + applicationContext, + conversationUser, roomId + ) + } else if (!TextUtils.isEmpty(roomToken)) { + NotificationUtils.cancelExistingNotificationsForRoom( + applicationContext, + conversationUser, roomToken!! + ) + } + } + } + + override fun onDetach(view: View) { + super.onDetach(view) + ApplicationWideCurrentRoomHolder.getInstance() + .clear() + + if (activity != null) { + activity?.findViewById(R.id.toolbar) + ?.setOnClickListener(null) + } + + if (conversationUser != null && conversationUser?.hasSpreedFeatureCapability("no-ping") + && activity != null && !activity?.isChangingConfigurations!! && !isLeavingForConversation + ) { + wasDetached = true + leaveRoom() + } + + if (mentionAutocomplete != null && mentionAutocomplete!!.isPopupShowing) { + mentionAutocomplete?.dismissPopup() + } + } + + override fun getTitle(): String? { + if (currentConversation != null && currentConversation?.displayName != null) { + return EmojiCompat.get() + .process(currentConversation!!.displayName) + .toString() + } else { + return "" + } + } + + public override fun onDestroy() { + super.onDestroy() + + if (activity != null) { + activity?.findViewById(R.id.toolbar) + ?.setOnClickListener(null) + } + + if (actionBar != null) { + actionBar?.setIcon(null) + } + + adapter = null + inConversation = false + } + + private fun startPing() { + if (conversationUser != null && !conversationUser.hasSpreedFeatureCapability("no-ping")) { + ncApi?.pingCall( + credentials, ApiUtils.getUrlForCallPing( + conversationUser.baseUrl, + roomToken + ) + ) + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.repeatWhen { observable -> observable.delay(5000, TimeUnit.MILLISECONDS) } + ?.takeWhile { observable -> inConversation } + ?.retry(3) { observable -> inConversation } + ?.`as`(AutoDispose.autoDisposable(scopeProvider)) + ?.subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + } + + override fun onNext(genericOverall: GenericOverall) { + + } + + override fun onError(e: Throwable) {} + + override fun onComplete() {} + }) + } + } + + @OnClick(R.id.smileyButton) + internal fun onSmileyClick() { + emojiPopup?.toggle() + } + + private fun joinRoomWithPassword() { + + if (currentConversation == null || TextUtils.isEmpty(currentConversation?.sessionId) || + currentConversation?.sessionId == "0" + ) { + ncApi?.joinRoom( + credentials, + ApiUtils.getUrlForSettingMyselfAsActiveParticipant(conversationUser?.baseUrl, roomToken), + roomPassword + ) + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.retry(3) + ?.`as`(AutoDispose.autoDisposable(scopeProvider)) + ?.subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + } + + override fun onNext(roomOverall: RoomOverall) { + inConversation = true + currentConversation?.sessionId = roomOverall.ocs.data.sessionId + + ApplicationWideCurrentRoomHolder.getInstance() + .session = + currentConversation?.sessionId + startPing() + + setupWebsocket() + checkLobbyState() + + if (isFirstMessagesProcessing) { + pullChatMessages(0) + } else { + pullChatMessages(1) + } + + if (magicWebSocketInstance != null) { + magicWebSocketInstance?.joinRoomWithRoomTokenAndSession( + roomToken, currentConversation?.sessionId + ) + } + if (startCallFromNotification != null && startCallFromNotification ?: false) { + startCallFromNotification = false + startACall(voiceOnly) + } + } + + override fun onError(e: Throwable) { + + } + + override fun onComplete() { + + } + }) + } else { + inConversation = true + ApplicationWideCurrentRoomHolder.getInstance() + .session = currentConversation?.sessionId + if (magicWebSocketInstance != null) { + magicWebSocketInstance?.joinRoomWithRoomTokenAndSession( + roomToken, + currentConversation?.sessionId + ) + } + startPing() + if (isFirstMessagesProcessing) { + pullChatMessages(0) + } else { + pullChatMessages(1) + } + } + } + + private fun leaveRoom() { + ncApi?.leaveRoom( + credentials, + ApiUtils.getUrlForSettingMyselfAsActiveParticipant( + conversationUser?.baseUrl, + roomToken + ) + ) + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + } + + override fun onNext(genericOverall: GenericOverall) { + checkingLobbyStatus = false + + if (lobbyTimerHandler != null) { + lobbyTimerHandler?.removeCallbacksAndMessages(null) + } + + if (magicWebSocketInstance != null && currentConversation != null) { + magicWebSocketInstance?.joinRoomWithRoomTokenAndSession( + "", + currentConversation?.sessionId + ) + } + + if (!isDestroyed && !isBeingDestroyed && !wasDetached) { + router.popCurrentController() + } + } + + override fun onError(e: Throwable) {} + + override fun onComplete() { + } + }) + } + + private fun setSenderId() { + try { + val senderId = adapter?.javaClass?.getDeclaredField("senderId") + senderId?.isAccessible = true + senderId?.set(adapter, conversationUser?.userId) + } catch (e: NoSuchFieldException) { + Log.w(TAG, "Failed to set sender id") + } catch (e: IllegalAccessException) { + Log.w(TAG, "Failed to access and set field") + } + + } + + private fun submitMessage() { + if (messageInput != null) { + val editable = messageInput!!.editableText + val mentionSpans = editable.getSpans( + 0, editable.length, + Spans.MentionChipSpan::class.java + ) + var mentionSpan: Spans.MentionChipSpan + for (i in mentionSpans.indices) { + mentionSpan = mentionSpans[i] + var mentionId = mentionSpan.id + if (mentionId.contains(" ") || mentionId.startsWith("guest/")) { + mentionId = "\"" + mentionId + "\"" + } + editable.replace( + editable.getSpanStart(mentionSpan), editable.getSpanEnd(mentionSpan), "@$mentionId" + ) + } + + messageInput?.setText("") + sendMessage(editable) + } + } + + private fun sendMessage(message: CharSequence) { + + if (conversationUser != null) { + ncApi?.sendChatMessage( + credentials, ApiUtils.getUrlForChat( + conversationUser?.baseUrl, + roomToken + ), + message, conversationUser.displayName + ) + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.`as`(AutoDispose.autoDisposable(scopeProvider)) + ?.subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + + } + + override fun onNext(genericOverall: GenericOverall) { + myFirstMessage = message + + if (popupBubble?.isShown ?: false) { + popupBubble?.hide() + } + + messagesListView?.smoothScrollToPosition(0) + } + + override fun onError(e: Throwable) { + if (e is HttpException) { + val code = e.code() + if (Integer.toString(code).startsWith("2")) { + myFirstMessage = message + + if (popupBubble?.isShown ?: false) { + popupBubble?.hide() + } + + messagesListView?.smoothScrollToPosition(0) + } + } + } + + override fun onComplete() { + + } + }) + } + } + + private fun setupWebsocket() { + if (conversationUser != null) { + if (WebSocketConnectionHelper.getMagicWebSocketInstanceForUserId( + conversationUser.id + ) != null + ) { + magicWebSocketInstance = + WebSocketConnectionHelper.getMagicWebSocketInstanceForUserId(conversationUser.id) + } else { + magicWebSocketInstance = null + } + } + } + + private fun pullChatMessages(lookIntoFuture: Int) { + if (!inConversation) { + return + } + + if (currentConversation != null && currentConversation!!.shouldShowLobby(conversationUser)) { + return + } + + val fieldMap = HashMap() + fieldMap["includeLastKnown"] = 0 + + var timeout = 30 + if (!lookingIntoFuture) { + timeout = 0; + } + + fieldMap["timeout"] = timeout + + if (lookIntoFuture > 0) { + lookingIntoFuture = true + } else if (isFirstMessagesProcessing) { + if (currentConversation != null) { + globalLastKnownFutureMessageId = currentConversation!!.lastReadMessage + globalLastKnownPastMessageId = currentConversation!!.lastReadMessage + fieldMap["includeLastKnown"] = 1 + } + } + + fieldMap["lookIntoFuture"] = lookIntoFuture + fieldMap["limit"] = 100 + fieldMap["setReadMarker"] = 1 + + val lastKnown: Int + if (lookIntoFuture > 0) { + lastKnown = globalLastKnownFutureMessageId + } else { + lastKnown = globalLastKnownPastMessageId + } + + fieldMap["lastKnownMessageId"] = lastKnown + + if (!wasDetached) { + if (lookIntoFuture > 0) { + val finalTimeout = timeout + ncApi?.pullChatMessages( + credentials, ApiUtils.getUrlForChat(conversationUser?.baseUrl, roomToken), fieldMap + ) + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.takeWhile { observable -> inConversation && !wasDetached } + ?.`as`(AutoDispose.autoDisposable(scopeProvider)) + ?.subscribe(object : Observer> { + override fun onSubscribe(d: Disposable) { + } + + override fun onNext(response: Response<*>) { + if (response.code() == 304) { + pullChatMessages(1) + } else { + processMessages(response, true, finalTimeout) + } + } + + override fun onError(e: Throwable) { + + } + + override fun onComplete() { + + } + }) + + } else { + ncApi?.pullChatMessages( + credentials, + ApiUtils.getUrlForChat(conversationUser?.baseUrl, roomToken), fieldMap + ) + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.retry(3) { observable -> inConversation && !wasDetached } + ?.takeWhile { observable -> inConversation && !wasDetached } + ?.`as`(AutoDispose.autoDisposable(scopeProvider)) + ?.subscribe(object : Observer> { + override fun onSubscribe(d: Disposable) { + } + + override fun onNext(response: Response<*>) { + processMessages(response, false, 0) + } + + override fun onError(e: Throwable) { + + } + + override fun onComplete() { + + } + }) + } + } + } + + private fun processMessages( + response: Response<*>, + isFromTheFuture: Boolean, + timeout: Int + ) { + val xChatLastGivenHeader: String? = response.headers() + .get("X-Chat-Last-Given") + if (response.headers().size > 0 && !TextUtils.isEmpty(xChatLastGivenHeader)) { + + val header = Integer.parseInt(xChatLastGivenHeader!!) + if (header > 0) { + if (isFromTheFuture) { + globalLastKnownFutureMessageId = header + } else { + globalLastKnownPastMessageId = header + } + } + } + + if (response.code() == 200) { + + val chatOverall = response.body() as ChatOverall? + val chatMessageList = chatOverall?.ocs!!.data + + val wasFirstMessageProcessing = isFirstMessagesProcessing + + if (isFirstMessagesProcessing) { cancelNotificationsForCurrentConversation() - if (inConversation) { - if (wasDetached && conversationUser?.hasSpreedFeatureCapability("no-ping") ?: false) { - currentConversation?.sessionId = "0" - wasDetached = false - joinRoomWithPassword() - } - } - } + isFirstMessagesProcessing = false + loadingProgressBar?.visibility = View.GONE - private fun cancelNotificationsForCurrentConversation() { - if (conversationUser != null) { - if (!conversationUser.hasSpreedFeatureCapability("no-ping") && !TextUtils.isEmpty(roomId)) { - NotificationUtils.cancelExistingNotificationsForRoom(applicationContext, - conversationUser, roomId) - } else if (!TextUtils.isEmpty(roomToken)) { - NotificationUtils.cancelExistingNotificationsForRoom(applicationContext, - conversationUser, roomToken!!) - } - } - } + messagesListView?.visibility = View.VISIBLE - override fun onDetach(view: View) { - super.onDetach(view) - ApplicationWideCurrentRoomHolder.getInstance().clear() - eventBus?.unregister(this) + } - if (activity != null) { - activity?.findViewById(R.id.toolbar)?.setOnClickListener(null) - } + var countGroupedMessages = 0 + if (!isFromTheFuture) { - if (conversationUser != null && conversationUser?.hasSpreedFeatureCapability("no-ping") - && activity != null && !activity?.isChangingConfigurations!! && !isLeavingForConversation) { - wasDetached = true - leaveRoom() - } - - if (mentionAutocomplete != null && mentionAutocomplete!!.isPopupShowing) { - mentionAutocomplete?.dismissPopup() - } - } - - override fun getTitle(): String? { - if (currentConversation != null && currentConversation?.displayName != null) { - return EmojiCompat.get().process(currentConversation!!.displayName).toString() - } else { - return "" - } - } - - public override fun onDestroy() { - super.onDestroy() - - if (activity != null) { - activity?.findViewById(R.id.toolbar)?.setOnClickListener(null) - } - - if (actionBar != null) { - actionBar?.setIcon(null) - } - - adapter = null - inConversation = false - } - - private fun startPing() { - if (conversationUser != null && !conversationUser.hasSpreedFeatureCapability("no-ping")) { - ncApi?.pingCall(credentials, ApiUtils.getUrlForCallPing(conversationUser.baseUrl, - roomToken)) - ?.subscribeOn(Schedulers.io()) - ?.observeOn(AndroidSchedulers.mainThread()) - ?.repeatWhen { observable -> observable.delay(5000, TimeUnit.MILLISECONDS) } - ?.takeWhile { observable -> inConversation } - ?.retry(3) { observable -> inConversation } - ?.`as`(AutoDispose.autoDisposable(scopeProvider)) - ?.subscribe(object : Observer { - override fun onSubscribe(d: Disposable) { - } - - override fun onNext(genericOverall: GenericOverall) { - - } - - override fun onError(e: Throwable) {} - - override fun onComplete() {} - }) - } - } - - @OnClick(R.id.smileyButton) - internal fun onSmileyClick() { - emojiPopup?.toggle() - } - - private fun joinRoomWithPassword() { - - if (currentConversation == null || TextUtils.isEmpty(currentConversation?.sessionId) || - currentConversation?.sessionId == "0") { - ncApi?.joinRoom(credentials, - ApiUtils.getUrlForSettingMyselfAsActiveParticipant(conversationUser?.baseUrl, roomToken), roomPassword) - ?.subscribeOn(Schedulers.io()) - ?.observeOn(AndroidSchedulers.mainThread()) - ?.retry(3) - ?.`as`(AutoDispose.autoDisposable(scopeProvider)) - ?.subscribe(object : Observer { - override fun onSubscribe(d: Disposable) { - } - - override fun onNext(roomOverall: RoomOverall) { - inConversation = true - currentConversation?.sessionId = roomOverall.ocs.data.sessionId - - ApplicationWideCurrentRoomHolder.getInstance().session = - currentConversation?.sessionId - startPing() - - setupWebsocket() - checkLobbyState() - - if (isFirstMessagesProcessing) { - pullChatMessages(0) - } else { - pullChatMessages(1) - } - - if (magicWebSocketInstance != null) { - magicWebSocketInstance?.joinRoomWithRoomTokenAndSession(roomToken, currentConversation?.sessionId) - } - if (startCallFromNotification != null && startCallFromNotification ?: false) { - startCallFromNotification = false - startACall(voiceOnly) - } - } - - override fun onError(e: Throwable) { - - } - - override fun onComplete() { - - } - }) - } else { - inConversation = true - ApplicationWideCurrentRoomHolder.getInstance().session = currentConversation?.sessionId - if (magicWebSocketInstance != null) { - magicWebSocketInstance?.joinRoomWithRoomTokenAndSession(roomToken, - currentConversation?.sessionId) - } - startPing() - if (isFirstMessagesProcessing) { - pullChatMessages(0) + for (i in chatMessageList.indices) { + if (chatMessageList.size > i + 1) { + if (TextUtils.isEmpty(chatMessageList[i].systemMessage) && + TextUtils.isEmpty(chatMessageList[i + 1].systemMessage) && + chatMessageList[i + 1].actorId == chatMessageList[i].actorId && + countGroupedMessages < 4 && DateFormatter.isSameDay( + chatMessageList[i].createdAt, + chatMessageList[i + 1].createdAt + ) + ) { + chatMessageList[i].isGrouped = true; + countGroupedMessages++ } else { - pullChatMessages(1) + countGroupedMessages = 0 } + } + + val chatMessage = chatMessageList[i] + chatMessage.isOneToOneConversation = + currentConversation?.type == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL + chatMessage.isLinkPreviewAllowed = isLinkPreviewAllowed + chatMessage.activeUser = conversationUser + } + + if (wasFirstMessageProcessing && chatMessageList.size > 0) { + globalLastKnownFutureMessageId = chatMessageList[0].jsonMessageId + } + + if (adapter != null) { + adapter?.addToEnd(chatMessageList, false) + } + + } else { + + var chatMessage: ChatMessage + + val shouldAddNewMessagesNotice = + timeout == 0 && adapter?.itemCount ?: 0 > 0 && chatMessageList.size > 0 + + if (shouldAddNewMessagesNotice) { + val unreadChatMessage = ChatMessage() + unreadChatMessage.jsonMessageId = -1 + unreadChatMessage.actorId = "-1" + unreadChatMessage.timestamp = chatMessageList[0].timestamp + unreadChatMessage.message = context?.getString(R.string.nc_new_messages) + adapter?.addToStart(unreadChatMessage, false) + } + + val isThereANewNotice = + shouldAddNewMessagesNotice || adapter?.getMessagePositionByIdInReverse("-1") != -1 + + for (i in chatMessageList.indices) { + chatMessage = chatMessageList[i] + + chatMessage.activeUser = conversationUser + chatMessage.isLinkPreviewAllowed = isLinkPreviewAllowed + + // if credentials are empty, we're acting as a guest + if (TextUtils.isEmpty(credentials) && myFirstMessage != null && !TextUtils.isEmpty( + myFirstMessage?.toString() + ) + ) { + if (chatMessage.actorType == "guests") { + conversationUser?.userId = chatMessage.actorId + setSenderId() + } + } + + val shouldScroll = + !isThereANewNotice && !shouldAddNewMessagesNotice && layoutManager?.findFirstVisibleItemPosition() == 0 || adapter != null && adapter?.itemCount == 0 + + if (!shouldAddNewMessagesNotice && !shouldScroll && popupBubble != null) { + if (!popupBubble!!.isShown) { + newMessagesCount = 1 + popupBubble?.show() + } else if (popupBubble!!.isShown) { + newMessagesCount++ + } + } else { + newMessagesCount = 0 + } + + if (adapter != null) { + chatMessage.isGrouped = (adapter!!.isPreviousSameAuthor( + chatMessage + .actorId, -1 + ) && adapter!!.getSameAuthorLastMessagesCount(chatMessage.actorId) % 5 > 0) + chatMessage.isOneToOneConversation = + (currentConversation?.type == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) + adapter?.addToStart(chatMessage, shouldScroll) + } + + } + + if (shouldAddNewMessagesNotice && adapter != null && messagesListView != null) { + layoutManager?.scrollToPositionWithOffset( + adapter!!.getMessagePositionByIdInReverse("-1"), messagesListView!!.height / 2 + ) + } + + } + + if (inConversation) { + pullChatMessages(1) + } + } else if (response.code() == 304 && !isFromTheFuture) { + if (isFirstMessagesProcessing) { + cancelNotificationsForCurrentConversation() + + isFirstMessagesProcessing = false + loadingProgressBar?.visibility = View.GONE + } + + historyRead = true + + if (!lookingIntoFuture && inConversation) { + pullChatMessages(1) + } } + } - private fun leaveRoom() { - ncApi?.leaveRoom(credentials, - ApiUtils.getUrlForSettingMyselfAsActiveParticipant(conversationUser?.baseUrl, - roomToken)) - ?.subscribeOn(Schedulers.io()) - ?.observeOn(AndroidSchedulers.mainThread()) - ?.subscribe(object : Observer { - override fun onSubscribe(d: Disposable) { - } - - override fun onNext(genericOverall: GenericOverall) { - checkingLobbyStatus = false - - if (lobbyTimerHandler != null) { - lobbyTimerHandler?.removeCallbacksAndMessages(null) - } - - if (magicWebSocketInstance != null && currentConversation != null) { - magicWebSocketInstance?.joinRoomWithRoomTokenAndSession("", - currentConversation?.sessionId) - } - - if (!isDestroyed && !isBeingDestroyed && !wasDetached) { - router.popCurrentController() - } - } - - override fun onError(e: Throwable) {} - - override fun onComplete() { - } - }) + override fun onLoadMore( + page: Int, + totalItemsCount: Int + ) { + if (!historyRead && inConversation) { + pullChatMessages(0) } + } - private fun setSenderId() { - try { - val senderId = adapter?.javaClass?.getDeclaredField("senderId") - senderId?.isAccessible = true - senderId?.set(adapter, conversationUser?.userId) - } catch (e: NoSuchFieldException) { - Log.w(TAG, "Failed to set sender id") - } catch (e: IllegalAccessException) { - Log.w(TAG, "Failed to access and set field") - } - + override fun format(date: Date): String { + return if (DateFormatter.isToday(date)) { + resources!!.getString(R.string.nc_date_header_today) + } else if (DateFormatter.isYesterday(date)) { + resources!!.getString(R.string.nc_date_header_yesterday) + } else { + DateFormatter.format(date, DateFormatter.Template.STRING_DAY_MONTH_YEAR) } + } - private fun submitMessage() { - if (messageInput != null) { - val editable = messageInput!!.editableText - val mentionSpans = editable.getSpans(0, editable.length, - Spans.MentionChipSpan::class.java) - var mentionSpan: Spans.MentionChipSpan - for (i in mentionSpans.indices) { - mentionSpan = mentionSpans[i] - var mentionId = mentionSpan.id - if (mentionId.contains(" ") || mentionId.startsWith("guest/")) { - mentionId = "\"" + mentionId + "\"" - } - editable.replace(editable.getSpanStart(mentionSpan), editable.getSpanEnd(mentionSpan), "@$mentionId") - } + override fun onCreateOptionsMenu( + menu: Menu, + inflater: MenuInflater + ) { + super.onCreateOptionsMenu(menu, inflater) + inflater.inflate(R.menu.menu_conversation, menu) + if (conversationUser?.userId == "?") { + menu.removeItem(R.id.conversation_info) + } else { + conversationInfoMenuItem = menu.findItem(R.id.conversation_info) + conversationVoiceCallMenuItem = menu.findItem(R.id.conversation_voice_call) + conversationVideoMenuItem = menu.findItem(R.id.conversation_video_call) - messageInput?.setText("") - sendMessage(editable) - } + loadAvatarForStatusBar() } + } - private fun sendMessage(message: CharSequence) { - - if (conversationUser != null) { - ncApi?.sendChatMessage(credentials, ApiUtils.getUrlForChat(conversationUser?.baseUrl, - roomToken), - message, conversationUser.displayName) - ?.subscribeOn(Schedulers.io()) - ?.observeOn(AndroidSchedulers.mainThread()) - ?.`as`(AutoDispose.autoDisposable(scopeProvider)) - ?.subscribe(object : Observer { - override fun onSubscribe(d: Disposable) { - - } - - override fun onNext(genericOverall: GenericOverall) { - myFirstMessage = message - - if (popupBubble?.isShown ?: false) { - popupBubble?.hide() - } - - messagesListView?.smoothScrollToPosition(0) - } - - override fun onError(e: Throwable) { - if (e is HttpException) { - val code = e.code() - if (Integer.toString(code).startsWith("2")) { - myFirstMessage = message - - if (popupBubble?.isShown ?: false) { - popupBubble?.hide() - } - - messagesListView?.smoothScrollToPosition(0) - } - } - } - - override fun onComplete() { - - } - }) - } + override fun onPrepareOptionsMenu(menu: Menu) { + super.onPrepareOptionsMenu(menu) + conversationUser?.let { + if (it.hasSpreedFeatureCapability("read-only-rooms")) { + checkReadOnlyState() + } } + } - private fun setupWebsocket() { - if (conversationUser != null) { - if (WebSocketConnectionHelper.getMagicWebSocketInstanceForUserId(conversationUser.id) != null) { - magicWebSocketInstance = WebSocketConnectionHelper.getMagicWebSocketInstanceForUserId(conversationUser.id) - } else { - magicWebSocketInstance = null - } + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + router.popCurrentController() + return true + } + R.id.conversation_video_call -> { + if (conversationVideoMenuItem?.icon?.alpha == 255) { + startACall(false) + return true } - } - - private fun pullChatMessages(lookIntoFuture: Int) { - if (!inConversation) { - return - } - - if (currentConversation != null && currentConversation!!.shouldShowLobby(conversationUser)) { - return - } - - val fieldMap = HashMap() - fieldMap["includeLastKnown"] = 0 - - var timeout = 30 - if (!lookingIntoFuture) { - timeout = 0; - } - - fieldMap["timeout"] = timeout - - if (lookIntoFuture > 0) { - lookingIntoFuture = true - } else if (isFirstMessagesProcessing) { - if (currentConversation != null) { - globalLastKnownFutureMessageId = currentConversation!!.lastReadMessage - globalLastKnownPastMessageId = currentConversation!!.lastReadMessage - fieldMap["includeLastKnown"] = 1 - } - } - - fieldMap["lookIntoFuture"] = lookIntoFuture - fieldMap["limit"] = 100 - fieldMap["setReadMarker"] = 1 - - val lastKnown: Int - if (lookIntoFuture > 0) { - lastKnown = globalLastKnownFutureMessageId - } else { - lastKnown = globalLastKnownPastMessageId - } - - fieldMap["lastKnownMessageId"] = lastKnown - - if (!wasDetached) { - if (lookIntoFuture > 0) { - val finalTimeout = timeout - ncApi?.pullChatMessages(credentials, ApiUtils.getUrlForChat(conversationUser?.baseUrl, roomToken), fieldMap) - ?.subscribeOn(Schedulers.io()) - ?.observeOn(AndroidSchedulers.mainThread()) - ?.takeWhile { observable -> inConversation && !wasDetached } - ?.`as`(AutoDispose.autoDisposable(scopeProvider)) - ?.subscribe(object : Observer> { - override fun onSubscribe(d: Disposable) { - } - - override fun onNext(response: Response<*>) { - if (response.code() == 304) { - pullChatMessages(1) - } else { - processMessages(response, true, finalTimeout) - } - } - - override fun onError(e: Throwable) { - - } - - override fun onComplete() { - - } - }) - - } else { - ncApi?.pullChatMessages(credentials, - ApiUtils.getUrlForChat(conversationUser?.baseUrl, roomToken), fieldMap) - ?.subscribeOn(Schedulers.io()) - ?.observeOn(AndroidSchedulers.mainThread()) - ?.retry(3) { observable -> inConversation && !wasDetached } - ?.takeWhile { observable -> inConversation && !wasDetached } - ?.`as`(AutoDispose.autoDisposable(scopeProvider)) - ?.subscribe(object : Observer> { - override fun onSubscribe(d: Disposable) { - } - - override fun onNext(response: Response<*>) { - processMessages(response, false, 0) - } - - override fun onError(e: Throwable) { - - } - - override fun onComplete() { - - } - }) - } - } - } - - private fun processMessages(response: Response<*>, isFromTheFuture: Boolean, timeout: Int) { - val xChatLastGivenHeader: String? = response.headers().get("X-Chat-Last-Given") - if (response.headers().size > 0 && !TextUtils.isEmpty(xChatLastGivenHeader)) { - - val header = Integer.parseInt(xChatLastGivenHeader!!) - if (header > 0) { - if (isFromTheFuture) { - globalLastKnownFutureMessageId = header - } else { - globalLastKnownPastMessageId = header - } - } - } - - if (response.code() == 200) { - - val chatOverall = response.body() as ChatOverall? - val chatMessageList = chatOverall?.ocs!!.data - - val wasFirstMessageProcessing = isFirstMessagesProcessing - - if (isFirstMessagesProcessing) { - cancelNotificationsForCurrentConversation() - - isFirstMessagesProcessing = false - loadingProgressBar?.visibility = View.GONE - - messagesListView?.visibility = View.VISIBLE - - } - - var countGroupedMessages = 0 - if (!isFromTheFuture) { - - for (i in chatMessageList.indices) { - if (chatMessageList.size > i + 1) { - if (TextUtils.isEmpty(chatMessageList[i].systemMessage) && - TextUtils.isEmpty(chatMessageList[i + 1].systemMessage) && - chatMessageList[i + 1].actorId == chatMessageList[i].actorId && - countGroupedMessages < 4 && DateFormatter.isSameDay(chatMessageList[i].createdAt, - chatMessageList[i + 1].createdAt)) { - chatMessageList[i].isGrouped = true; - countGroupedMessages++ - } else { - countGroupedMessages = 0 - } - } - - val chatMessage = chatMessageList[i] - chatMessage.isOneToOneConversation = currentConversation?.type == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL - chatMessage.isLinkPreviewAllowed = isLinkPreviewAllowed - chatMessage.activeUser = conversationUser - - } - - if (wasFirstMessageProcessing && chatMessageList.size > 0) { - globalLastKnownFutureMessageId = chatMessageList[0].jsonMessageId - } - - if (adapter != null) { - adapter?.addToEnd(chatMessageList, false) - } - - } else { - - var chatMessage: ChatMessage - - val shouldAddNewMessagesNotice = timeout == 0 && adapter?.itemCount ?: 0 > 0 && chatMessageList.size > 0 - - if (shouldAddNewMessagesNotice) { - val unreadChatMessage = ChatMessage() - unreadChatMessage.jsonMessageId = -1 - unreadChatMessage.actorId = "-1" - unreadChatMessage.timestamp = chatMessageList[0].timestamp - unreadChatMessage.message = context?.getString(R.string.nc_new_messages) - adapter?.addToStart(unreadChatMessage, false) - } - - val isThereANewNotice = shouldAddNewMessagesNotice || adapter?.getMessagePositionByIdInReverse("-1") != -1 - - for (i in chatMessageList.indices) { - chatMessage = chatMessageList[i] - - chatMessage.activeUser = conversationUser - chatMessage.isLinkPreviewAllowed = isLinkPreviewAllowed - - // if credentials are empty, we're acting as a guest - if (TextUtils.isEmpty(credentials) && myFirstMessage != null && !TextUtils.isEmpty(myFirstMessage?.toString())) { - if (chatMessage.actorType == "guests") { - conversationUser?.userId = chatMessage.actorId - setSenderId() - } - } - - val shouldScroll = !isThereANewNotice && !shouldAddNewMessagesNotice && layoutManager?.findFirstVisibleItemPosition() == 0 || adapter != null && adapter?.itemCount == 0 - - if (!shouldAddNewMessagesNotice && !shouldScroll && popupBubble != null) { - if (!popupBubble!!.isShown) { - newMessagesCount = 1 - popupBubble?.show() - } else if (popupBubble!!.isShown) { - newMessagesCount++ - } - } else { - newMessagesCount = 0 - } - - if (adapter != null) { - chatMessage.isGrouped = (adapter!!.isPreviousSameAuthor(chatMessage - .actorId, -1) && adapter!!.getSameAuthorLastMessagesCount(chatMessage.actorId) % 5 > 0) - chatMessage.isOneToOneConversation = (currentConversation?.type == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) - adapter?.addToStart(chatMessage, shouldScroll) - } - - } - - if (shouldAddNewMessagesNotice && adapter != null && messagesListView != null) { - layoutManager?.scrollToPositionWithOffset(adapter!!.getMessagePositionByIdInReverse("-1"), messagesListView!!.height / 2) - } - - } - - if (inConversation) { - pullChatMessages(1) - } - } else if (response.code() == 304 && !isFromTheFuture) { - if (isFirstMessagesProcessing) { - cancelNotificationsForCurrentConversation() - - isFirstMessagesProcessing = false - loadingProgressBar?.visibility = View.GONE - } - - historyRead = true - - if (!lookingIntoFuture && inConversation) { - pullChatMessages(1) - } - } - } - - override fun onLoadMore(page: Int, totalItemsCount: Int) { - if (!historyRead && inConversation) { - pullChatMessages(0) - } - } - - - override fun format(date: Date): String { - return if (DateFormatter.isToday(date)) { - resources!!.getString(R.string.nc_date_header_today) - } else if (DateFormatter.isYesterday(date)) { - resources!!.getString(R.string.nc_date_header_yesterday) - } else { - DateFormatter.format(date, DateFormatter.Template.STRING_DAY_MONTH_YEAR) - } - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - super.onCreateOptionsMenu(menu, inflater) - inflater.inflate(R.menu.menu_conversation, menu) - if (conversationUser?.userId == "?") { - menu.removeItem(R.id.conversation_info) - } else { - conversationInfoMenuItem = menu.findItem(R.id.conversation_info) - conversationVoiceCallMenuItem = menu.findItem(R.id.conversation_voice_call) - conversationVideoMenuItem = menu.findItem(R.id.conversation_video_call) - - loadAvatarForStatusBar() - } - } - - override fun onPrepareOptionsMenu(menu: Menu) { - super.onPrepareOptionsMenu(menu) - conversationUser?.let { - if (it.hasSpreedFeatureCapability("read-only-rooms")) { - checkReadOnlyState() - } - } - } - - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - android.R.id.home -> { - router.popCurrentController() - return true - } - R.id.conversation_video_call -> { - if (conversationVideoMenuItem?.icon?.alpha == 255) { - startACall(false) - return true - } - return false - } - R.id.conversation_voice_call -> { - if (conversationVoiceCallMenuItem?.icon?.alpha == 255) { - startACall(true) - return true - } - return false - } - R.id.conversation_info -> { - showConversationInfoScreen() - return true - } - else -> return super.onOptionsItemSelected(item) - } - } - - private fun startACall(isVoiceOnlyCall: Boolean) { - isLeavingForConversation = true - if (!isVoiceOnlyCall) { - val videoCallIntent = getIntentForCall(false) - if (videoCallIntent != null) { - startActivity(videoCallIntent) - } - } else { - val voiceCallIntent = getIntentForCall(true) - if (voiceCallIntent != null) { - startActivity(voiceCallIntent) - } - } - } - - private fun getIntentForCall(isVoiceOnlyCall: Boolean): Intent? { - if (currentConversation != null) { - val bundle = Bundle() - bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomToken) - bundle.putString(BundleKeys.KEY_ROOM_ID, roomId) - bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, conversationUser) - bundle.putString(BundleKeys.KEY_CONVERSATION_PASSWORD, roomPassword) - bundle.putString(BundleKeys.KEY_MODIFIED_BASE_URL, conversationUser?.baseUrl) - - if (isVoiceOnlyCall) { - bundle.putBoolean(BundleKeys.KEY_CALL_VOICE_ONLY, true) - } - - if (activity != null) { - val callIntent = Intent(activity, MagicCallActivity::class.java) - callIntent.putExtras(bundle) - - return callIntent - } else { - return null - } - } else { - return null - } - } - - override fun onMessageLongClick(message: IMessage) { - if (activity != null) { - val clipboardManager = activity?.getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager - val clipData = android.content.ClipData.newPlainText( - resources?.getString(R.string.nc_app_name), message.text) - clipboardManager.setPrimaryClip(clipData) - } - } - - override fun hasContentFor(message: IMessage, type: Byte): Boolean { - when (type) { - CONTENT_TYPE_SYSTEM_MESSAGE -> return !TextUtils.isEmpty(message.systemMessage) - CONTENT_TYPE_UNREAD_NOTICE_MESSAGE -> return message.id == "-1" - } - return false - } - - @Subscribe(threadMode = ThreadMode.BACKGROUND) - fun onMessageEvent(webSocketCommunicationEvent: WebSocketCommunicationEvent) { - /* - switch (webSocketCommunicationEvent.getType()) { - case "refreshChat": - - if (webSocketCommunicationEvent.getHashMap().get(BundleKeys.KEY_INTERNAL_USER_ID).equals(Long.toString(conversationUser.getId()))) { - if (roomToken.equals(webSocketCommunicationEvent.getHashMap().get(BundleKeys.KEY_ROOM_TOKEN))) { - pullChatMessages(2); - } - } - break; - default: - }*/ - } - - @Subscribe(threadMode = ThreadMode.BACKGROUND) - fun onMessageEvent(userMentionClickEvent: UserMentionClickEvent) { - if (currentConversation?.type != Conversation.ConversationType - .ROOM_TYPE_ONE_TO_ONE_CALL || currentConversation?.name != - userMentionClickEvent.userId) { - val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(conversationUser?.baseUrl, "1", - userMentionClickEvent.userId, null) - - ncApi?.createRoom(credentials, - retrofitBucket.url, retrofitBucket.queryMap) - ?.subscribeOn(Schedulers.io()) - ?.observeOn(AndroidSchedulers.mainThread()) - ?.`as`(AutoDispose.autoDisposable(scopeProvider)) - ?.subscribe(object : Observer { - override fun onSubscribe(d: Disposable) { - - } - - override fun onNext(roomOverall: RoomOverall) { - val conversationIntent = Intent(activity, MagicCallActivity::class.java) - val bundle = Bundle() - bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, conversationUser) - bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomOverall.ocs.data.token) - bundle.putString(BundleKeys.KEY_ROOM_ID, roomOverall.ocs.data.roomId) - - if (conversationUser != null) { - if (conversationUser.hasSpreedFeatureCapability("chat-v2")) { - bundle.putParcelable(BundleKeys.KEY_ACTIVE_CONVERSATION, - Parcels.wrap(roomOverall.ocs.data)) - conversationIntent.putExtras(bundle) - - ConductorRemapping.remapChatController(router, conversationUser.id, - roomOverall.ocs.data.token, bundle, false) - } - - } else { - conversationIntent.putExtras(bundle) - startActivity(conversationIntent) - Handler().postDelayed({ - if (!isDestroyed && !isBeingDestroyed) { - router.popCurrentController() - } - }, 100) - } - } - - override fun onError(e: Throwable) { - - } - - override fun onComplete() {} - }) + } + R.id.conversation_voice_call -> { + if (conversationVoiceCallMenuItem?.icon?.alpha == 255) { + startACall(true) + return true } + return false + } + R.id.conversation_info -> { + showConversationInfoScreen() + return true + } + else -> return super.onOptionsItemSelected(item) + } + } + + private fun startACall(isVoiceOnlyCall: Boolean) { + isLeavingForConversation = true + if (!isVoiceOnlyCall) { + val videoCallIntent = getIntentForCall(false) + if (videoCallIntent != null) { + startActivity(videoCallIntent) + } + } else { + val voiceCallIntent = getIntentForCall(true) + if (voiceCallIntent != null) { + startActivity(voiceCallIntent) + } + } + } + + private fun getIntentForCall(isVoiceOnlyCall: Boolean): Intent? { + if (currentConversation != null) { + val bundle = Bundle() + bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomToken) + bundle.putString(BundleKeys.KEY_ROOM_ID, roomId) + bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, conversationUser) + bundle.putString(BundleKeys.KEY_CONVERSATION_PASSWORD, roomPassword) + bundle.putString(BundleKeys.KEY_MODIFIED_BASE_URL, conversationUser?.baseUrl) + + if (isVoiceOnlyCall) { + bundle.putBoolean(BundleKeys.KEY_CALL_VOICE_ONLY, true) + } + + if (activity != null) { + val callIntent = Intent(activity, MagicCallActivity::class.java) + callIntent.putExtras(bundle) + + return callIntent + } else { + return null + } + } else { + return null + } + } + + override fun onMessageLongClick(message: IMessage) { + if (activity != null) { + val clipboardManager = + activity?.getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager + val clipData = android.content.ClipData.newPlainText( + resources?.getString(R.string.nc_app_name), message.text + ) + clipboardManager.setPrimaryClip(clipData) + } + } + + override fun hasContentFor( + message: IMessage, + type: Byte + ): Boolean { + when (type) { + CONTENT_TYPE_SYSTEM_MESSAGE -> return !TextUtils.isEmpty(message.systemMessage) + CONTENT_TYPE_UNREAD_NOTICE_MESSAGE -> return message.id == "-1" } - companion object { - private val TAG = "ChatController" - private val CONTENT_TYPE_SYSTEM_MESSAGE: Byte = 1 - private val CONTENT_TYPE_UNREAD_NOTICE_MESSAGE: Byte = 2 + return false + } + + @Subscribe(threadMode = ThreadMode.BACKGROUND) + fun onMessageEvent(webSocketCommunicationEvent: WebSocketCommunicationEvent) { + /* + switch (webSocketCommunicationEvent.getType()) { + case "refreshChat": + + if (webSocketCommunicationEvent.getHashMap().get(BundleKeys.KEY_INTERNAL_USER_ID).equals(Long.toString(conversationUser.getId()))) { + if (roomToken.equals(webSocketCommunicationEvent.getHashMap().get(BundleKeys.KEY_ROOM_TOKEN))) { + pullChatMessages(2); + } + } + break; + default: + }*/ + } + + @Subscribe(threadMode = ThreadMode.BACKGROUND) + fun onMessageEvent(userMentionClickEvent: UserMentionClickEvent) { + if (currentConversation?.type != Conversation.ConversationType + .ROOM_TYPE_ONE_TO_ONE_CALL || currentConversation?.name != + userMentionClickEvent.userId + ) { + val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom( + conversationUser?.baseUrl, "1", + userMentionClickEvent.userId, null + ) + + ncApi?.createRoom( + credentials, + retrofitBucket.url, retrofitBucket.queryMap + ) + ?.subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.`as`(AutoDispose.autoDisposable(scopeProvider)) + ?.subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + + } + + override fun onNext(roomOverall: RoomOverall) { + val conversationIntent = Intent(activity, MagicCallActivity::class.java) + val bundle = Bundle() + bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, conversationUser) + bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomOverall.ocs.data.token) + bundle.putString(BundleKeys.KEY_ROOM_ID, roomOverall.ocs.data.roomId) + + if (conversationUser != null) { + if (conversationUser.hasSpreedFeatureCapability("chat-v2")) { + bundle.putParcelable( + BundleKeys.KEY_ACTIVE_CONVERSATION, + Parcels.wrap(roomOverall.ocs.data) + ) + conversationIntent.putExtras(bundle) + + ConductorRemapping.remapChatController( + router, conversationUser.id, + roomOverall.ocs.data.token, bundle, false + ) + } + + } else { + conversationIntent.putExtras(bundle) + startActivity(conversationIntent) + Handler().postDelayed({ + if (!isDestroyed && !isBeingDestroyed) { + router.popCurrentController() + } + }, 100) + } + } + + override fun onError(e: Throwable) { + + } + + override fun onComplete() {} + }) } + } + + companion object { + private val TAG = "ChatController" + private val CONTENT_TYPE_SYSTEM_MESSAGE: Byte = 1 + private val CONTENT_TYPE_UNREAD_NOTICE_MESSAGE: Byte = 2 + } } 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 69ac81bc6..60c2be3da 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ContactsController.java +++ b/app/src/main/java/com/nextcloud/talk/controllers/ContactsController.java @@ -213,7 +213,6 @@ public class ContactsController extends BaseController implements SearchView.OnQ @Override protected void onAttach(@NonNull View view) { super.onAttach(view); - eventBus.register(this); if (isNewConversationView) { toggleNewCallHeaderVisibility(!isPublicCall); @@ -283,7 +282,7 @@ public class ContactsController extends BaseController implements SearchView.OnQ retrofitBucket.getUrl(), retrofitBucket.getQueryMap()) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .as(AutoDispose.autoDisposable(scopeProvider)) + .as(AutoDispose.autoDisposable(getScopeProvider())) .subscribe(new Observer() { @Override public void onSubscribe(Disposable d) { @@ -304,7 +303,7 @@ public class ContactsController extends BaseController implements SearchView.OnQ roomOverall.getOcs().getData().getToken())) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .as(AutoDispose.autoDisposable(scopeProvider)) + .as(AutoDispose.autoDisposable(getScopeProvider())) .subscribe(new Observer() { @Override @@ -501,7 +500,7 @@ public class ContactsController extends BaseController implements SearchView.OnQ .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .retry(3) - .as(AutoDispose.autoDisposable(scopeProvider)) + .as(AutoDispose.autoDisposable(getScopeProvider())) .subscribe(new Observer() { @Override public void onSubscribe(Disposable d) { @@ -787,7 +786,7 @@ public class ContactsController extends BaseController implements SearchView.OnQ } @Override - protected String getTitle() { + public String getTitle() { if (!isNewConversationView && !isAddingParticipantsView) { return getResources().getString(R.string.nc_app_name); } else { @@ -855,12 +854,6 @@ public class ContactsController extends BaseController implements SearchView.OnQ } } - @Override - protected void onDetach(@NonNull View view) { - super.onDetach(view); - eventBus.unregister(this); - } - @Override public boolean onItemClick(View view, int position) { if (adapter.getItem(position) instanceof UserItem) { @@ -878,7 +871,7 @@ public class ContactsController extends BaseController implements SearchView.OnQ retrofitBucket.getUrl(), retrofitBucket.getQueryMap()) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .as(AutoDispose.autoDisposable(scopeProvider)) + .as(AutoDispose.autoDisposable(getScopeProvider())) .subscribe(new Observer() { @Override public void onSubscribe(Disposable d) { 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 e948788cd..a3464dd9d 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt @@ -130,10 +130,6 @@ class ConversationInfoController(args: Bundle) : BaseController(), FlexibleAdapt @set:Inject lateinit var ncApi: NcApi - @set:Inject - lateinit var context: Context - @set:Inject - lateinit var eventBus: EventBus private val conversationToken: String? private val conversationUser: UserEntity? @@ -185,7 +181,6 @@ class ConversationInfoController(args: Bundle) : BaseController(), FlexibleAdapt override fun onAttach(view: View) { super.onAttach(view) - eventBus.register(this) if (databaseStorageModule == null) { databaseStorageModule = DatabaseStorageModule(conversationUser!!, conversationToken) @@ -306,11 +301,6 @@ class ConversationInfoController(args: Bundle) : BaseController(), FlexibleAdapt getListOfParticipants() } - override fun onDetach(view: View) { - super.onDetach(view) - eventBus.unregister(this) - } - private fun showDeleteConversationDialog(savedInstanceState: Bundle?) { if (activity != null) { LovelyStandardDialog(activity, LovelyStandardDialog.ButtonLayout.HORIZONTAL) 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 e6887ee91..895b8c601 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.java +++ b/app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.java @@ -236,8 +236,6 @@ public class ConversationsListController extends BaseController implements Searc @Override protected void onAttach(@NonNull View view) { super.onAttach(view); - eventBus.register(this); - currentUser = userUtils.getCurrentUser(); if (currentUser != null) { @@ -247,11 +245,6 @@ public class ConversationsListController extends BaseController implements Searc } } - @Override - protected void onDetach(@NonNull View view) { - super.onDetach(view); - eventBus.unregister(this); - } private void initSearchView() { if (getActivity() != null) { @@ -318,7 +311,7 @@ public class ConversationsListController extends BaseController implements Searc ncApi.getRooms(credentials, ApiUtils.getUrlForGetRooms(currentUser.getBaseUrl())) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .as(AutoDispose.autoDisposable(scopeProvider)) + .as(AutoDispose.autoDisposable(getScopeProvider())) .subscribe(roomsOverall -> { if (adapterWasNull) { @@ -574,7 +567,7 @@ public class ConversationsListController extends BaseController implements Searc @Override - protected String getTitle() { + public String getTitle() { return getResources().getString(R.string.nc_app_name); } 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 4876e27b9..78c5d0099 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/RingtoneSelectionController.java +++ b/app/src/main/java/com/nextcloud/talk/controllers/RingtoneSelectionController.java @@ -225,7 +225,7 @@ public class RingtoneSelectionController extends BaseController implements Flexi } @Override - protected String getTitle() { + public String getTitle() { return getResources().getString(R.string.nc_settings_notification_sounds); } diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ServerSelectionController.java b/app/src/main/java/com/nextcloud/talk/controllers/ServerSelectionController.java index e6cfd8ee5..f6998b6e6 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ServerSelectionController.java +++ b/app/src/main/java/com/nextcloud/talk/controllers/ServerSelectionController.java @@ -243,7 +243,7 @@ public class ServerSelectionController extends BaseController { ncApi.getServerStatus(queryUrl) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .as(AutoDispose.autoDisposable(scopeProvider)) + .as(AutoDispose.autoDisposable(getScopeProvider())) .subscribe(status -> { String productName = getResources().getString(R.string.nc_server_product_name); 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 5420d6050..5fe4ec3c4 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/SettingsController.java +++ b/app/src/main/java/com/nextcloud/talk/controllers/SettingsController.java @@ -510,7 +510,7 @@ public class SettingsController extends BaseController { ApiUtils.getUrlForUserProfile(currentUser.getBaseUrl())) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .as(AutoDispose.autoDisposable(scopeProvider)) + .as(AutoDispose.autoDisposable(getScopeProvider())) .subscribe(userProfileOverall -> { String displayName = null; @@ -533,7 +533,7 @@ public class SettingsController extends BaseController { null, currentUser.getId(), null, null, null) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .as(AutoDispose.autoDisposable(scopeProvider)) + .as(AutoDispose.autoDisposable(getScopeProvider())) .subscribe(userEntityResult -> { displayNameTextView.setText(userEntityResult.getDisplayName()); }, @@ -667,7 +667,7 @@ public class SettingsController extends BaseController { } @Override - protected String getTitle() { + public String getTitle() { return getResources().getString(R.string.nc_settings); } 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 5cac427b7..62a48cf93 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/SwitchAccountController.java +++ b/app/src/main/java/com/nextcloud/talk/controllers/SwitchAccountController.java @@ -97,7 +97,7 @@ public class SwitchAccountController extends BaseController { userUtils.createOrUpdateUser(null, null, null, null, null, true, null, userEntity.getId(), null, null, null) - .as(AutoDispose.autoDisposable(scopeProvider)) + .as(AutoDispose.autoDisposable(getScopeProvider())) .subscribe(new Observer() { @Override public void onSubscribe(Disposable d) { @@ -243,7 +243,7 @@ public class SwitchAccountController extends BaseController { } @Override - protected String getTitle() { + public String getTitle() { return getResources().getString(R.string.nc_select_an_account); } } diff --git a/app/src/main/java/com/nextcloud/talk/controllers/WebViewLoginController.java b/app/src/main/java/com/nextcloud/talk/controllers/WebViewLoginController.java index 348447c5b..84c701cde 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/WebViewLoginController.java +++ b/app/src/main/java/com/nextcloud/talk/controllers/WebViewLoginController.java @@ -392,7 +392,7 @@ public class WebViewLoginController extends BaseController { null, currentUser.getId(), null, appPreferences.getTemporaryClientCertAlias(), null) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .as(AutoDispose.autoDisposable(scopeProvider)) + .as(AutoDispose.autoDisposable(getScopeProvider())) .subscribe(userEntity -> { if (finalMessageType != null) { ApplicationWideMessageHolder.getInstance().setMessageType(finalMessageType); diff --git a/app/src/main/java/com/nextcloud/talk/controllers/base/BaseController.java b/app/src/main/java/com/nextcloud/talk/controllers/base/BaseController.java deleted file mode 100644 index 2191eb4bd..000000000 --- a/app/src/main/java/com/nextcloud/talk/controllers/base/BaseController.java +++ /dev/null @@ -1,166 +0,0 @@ -/** - * Nextcloud Talk application - * - * @author BlueLine Labs, Inc. - * Copyright (C) 2016 BlueLine Labs, Inc. - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.nextcloud.talk.controllers.base; - -import android.content.Context; -import android.os.Build; -import android.util.Log; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputMethodManager; -import android.widget.EditText; -import androidx.annotation.NonNull; -import androidx.annotation.RequiresApi; -import androidx.appcompat.app.ActionBar; -import autodagger.AutoInjector; -import com.bluelinelabs.conductor.Controller; -import com.bluelinelabs.conductor.autodispose.ControllerScopeProvider; -import com.nextcloud.talk.application.NextcloudTalkApplication; -import com.nextcloud.talk.controllers.AccountVerificationController; -import com.nextcloud.talk.controllers.ServerSelectionController; -import com.nextcloud.talk.controllers.SwitchAccountController; -import com.nextcloud.talk.controllers.WebViewLoginController; -import com.nextcloud.talk.controllers.base.providers.ActionBarProvider; -import com.nextcloud.talk.utils.preferences.AppPreferences; -import com.uber.autodispose.lifecycle.LifecycleScopeProvider; -import java.util.ArrayList; -import java.util.List; -import javax.inject.Inject; - -@AutoInjector(NextcloudTalkApplication.class) -public abstract class BaseController extends ButterKnifeController { - - private static final String TAG = "BaseController"; - - public final LifecycleScopeProvider scopeProvider = ControllerScopeProvider.from(this); - - @Inject - AppPreferences appPreferences; - @Inject - Context context; - - @Override - protected void onContextAvailable(Context context) { - super.onContextAvailable(context); - NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this); - cleanTempCertPreference(); - } - - @Override - public boolean onOptionsItemSelected(@NonNull MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - getRouter().popCurrentController(); - return true; - default: - return super.onOptionsItemSelected(item); - } - } - - private void cleanTempCertPreference() { - List temporaryClassNames = new ArrayList<>(); - temporaryClassNames.add(ServerSelectionController.class.getName()); - temporaryClassNames.add(AccountVerificationController.class.getName()); - temporaryClassNames.add(WebViewLoginController.class.getName()); - temporaryClassNames.add(SwitchAccountController.class.getName()); - - if (!temporaryClassNames.contains(getClass().getName())) { - appPreferences.removeTemporaryClientCertAlias(); - } - - } - - @Override - protected void onViewBound(@NonNull View view) { - super.onViewBound(view); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && appPreferences.getIsKeyboardIncognito()) { - disableKeyboardPersonalisedLearning((ViewGroup) view); - } - } - - // Note: This is just a quick demo of how an ActionBar *can* be accessed, not necessarily how it *should* - // be accessed. In a production app, this would use Dagger instead. - protected ActionBar getActionBar() { - ActionBarProvider actionBarProvider = null; - try { - actionBarProvider = ((ActionBarProvider) getActivity()); - } catch (Exception exception) { - Log.d(TAG, "Failed to fetch the action bar provider"); - } - return actionBarProvider != null ? actionBarProvider.getSupportActionBar() : null; - } - - @Override - protected void onAttach(@NonNull View view) { - super.onAttach(view); - - setTitle(); - if (getActionBar() != null) { - getActionBar().setDisplayHomeAsUpEnabled(getParentController() != null || getRouter().getBackstackSize() > 1); - } - } - - @Override - protected void onDetach(@NonNull View view) { - super.onDetach(view); - InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); - if (imm != null) { - imm.hideSoftInputFromWindow(view.getWindowToken(), 0); - } - } - - protected void setTitle() { - Controller parentController = getParentController(); - while (parentController != null) { - if (parentController instanceof BaseController && ((BaseController) parentController).getTitle() != null) { - return; - } - parentController = parentController.getParentController(); - } - - String title = getTitle(); - ActionBar actionBar = getActionBar(); - if (title != null && actionBar != null) { - actionBar.setTitle(title); - } - } - - protected String getTitle() { - return null; - } - - @RequiresApi(api = Build.VERSION_CODES.O) - private void disableKeyboardPersonalisedLearning(final ViewGroup viewGroup) { - View view; - EditText editText; - - for (int i = 0; i < viewGroup.getChildCount(); i++) { - view = viewGroup.getChildAt(i); - if (view instanceof EditText) { - editText = (EditText) view; - editText.setImeOptions(editText.getImeOptions() | EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING); - } else if (view instanceof ViewGroup) { - disableKeyboardPersonalisedLearning((ViewGroup) view); - } - } - } -} 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 new file mode 100644 index 000000000..32d5d3f95 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/controllers/base/BaseController.kt @@ -0,0 +1,170 @@ +/** + * Nextcloud Talk application + * + * @author BlueLine Labs, Inc. + * Copyright (C) 2016 BlueLine Labs, Inc. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.nextcloud.talk.controllers.base + +import android.content.Context +import android.os.Build +import android.util.Log +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputMethodManager +import android.widget.EditText +import androidx.annotation.RequiresApi +import androidx.appcompat.app.ActionBar +import autodagger.AutoInjector +import com.bluelinelabs.conductor.autodispose.ControllerScopeProvider +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.controllers.AccountVerificationController +import com.nextcloud.talk.controllers.ServerSelectionController +import com.nextcloud.talk.controllers.SwitchAccountController +import com.nextcloud.talk.controllers.WebViewLoginController +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 java.util.ArrayList +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +abstract class BaseController : ButterKnifeController(), KoinComponent { + + val scopeProvider: LifecycleScopeProvider<*> = ControllerScopeProvider.from(this) + + val appPreferences: AppPreferences by inject() + val context: Context by inject() + val eventBus: EventBus by inject() + + // Note: This is just a quick demo of how an ActionBar *can* be accessed, not necessarily how it *should* + // be accessed. In a production app, this would use Dagger instead. + protected val actionBar: ActionBar? + get() { + var actionBarProvider: ActionBarProvider? = null + try { + actionBarProvider = activity as ActionBarProvider? + } catch (exception: Exception) { + Log.d(TAG, "Failed to fetch the action bar provider") + } + + return actionBarProvider?.supportActionBar + } + + override fun getKoin(): Koin { + return super.getKoin() + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + router.popCurrentController() + return true + } + else -> return super.onOptionsItemSelected(item) + } + } + + private fun cleanTempCertPreference() { + val temporaryClassNames = ArrayList() + temporaryClassNames.add(ServerSelectionController::class.java.name) + temporaryClassNames.add(AccountVerificationController::class.java.name) + temporaryClassNames.add(WebViewLoginController::class.java.name) + temporaryClassNames.add(SwitchAccountController::class.java.name) + + if (!temporaryClassNames.contains(javaClass.name)) { + appPreferences.removeTemporaryClientCertAlias() + } + + } + + override fun onViewBound(view: View) { + super.onViewBound(view) + cleanTempCertPreference() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && appPreferences.isKeyboardIncognito) { + disableKeyboardPersonalisedLearning(view as ViewGroup) + } + } + + override fun onAttach(view: View) { + super.onAttach(view) + eventBus.register(this) + + setTitle() + if (actionBar != null) { + actionBar!!.setDisplayHomeAsUpEnabled(parentController != null || router.backstackSize > 1) + } + } + + 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) + } + + protected fun setTitle() { + var parentController = parentController + while (parentController != null) { + if (parentController is BaseController && parentController.getTitle() != null) { + return + } + parentController = parentController.parentController + } + + val title = getTitle() + val actionBar = actionBar + if (title != null && actionBar != null) { + actionBar.title = title + } + } + + @RequiresApi(api = Build.VERSION_CODES.O) + private fun disableKeyboardPersonalisedLearning(viewGroup: ViewGroup) { + var view: View + var editText: EditText + + for (i in 0 until viewGroup.childCount) { + view = viewGroup.getChildAt(i) + if (view is EditText) { + editText = view + editText.imeOptions = editText.imeOptions or EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING + } else if (view is ViewGroup) { + disableKeyboardPersonalisedLearning(view) + } + } + } + + companion object { + + private val TAG = "BaseController" + } + + open fun getTitle(): String? { + return null + } +} diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/RestModule.java b/app/src/main/java/com/nextcloud/talk/dagger/modules/RestModule.java index 42e514621..96e375b09 100644 --- a/app/src/main/java/com/nextcloud/talk/dagger/modules/RestModule.java +++ b/app/src/main/java/com/nextcloud/talk/dagger/modules/RestModule.java @@ -43,6 +43,7 @@ import dagger.Provides; import io.reactivex.schedulers.Schedulers; import java.io.IOException; import java.net.CookieManager; +import java.net.CookiePolicy; import java.net.InetSocketAddress; import java.net.Proxy; import java.security.KeyStore; @@ -159,14 +160,15 @@ public class RestModule { @Singleton @Provides CookieManager provideCookieManager() { - return new CookieManager(); + CookieManager cookieManager = new CookieManager(); + cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_NONE); + return cookieManager; } @Singleton @Provides Cache provideCache() { int cacheSize = 128 * 1024 * 1024; // 128 MB - return new Cache(NextcloudTalkApplication.Companion.getSharedApplication().getCacheDir(), cacheSize); } diff --git a/app/src/main/java/com/nextcloud/talk/newarch/data/model/ErrorModel.kt b/app/src/main/java/com/nextcloud/talk/newarch/data/model/ErrorModel.kt new file mode 100644 index 000000000..ca138cdff --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/newarch/data/model/ErrorModel.kt @@ -0,0 +1,89 @@ +/* + * 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 . + * + * Inspired by: https://github.com/ZahraHeydari/Android-Clean-Arch-Coroutines-Koin/blob/c63e62bf8fb9c099a983fcc7d1b5616a7318aa3f/app/src/main/java/com/android/post/data/model/ErrorModel.kt + */ + +package com.nextcloud.talk.newarch.data.model + +/** + * This class designed to show different types of errors through error status & message + * + * */ + +private const val NO_CONNECTION_ERROR_MESSAGE = "No connection" +private const val BAD_RESPONSE_ERROR_MESSAGE = "Bad response" +private const val TIME_OUT_ERROR_MESSAGE = "Time out" +private const val EMPTY_RESPONSE_ERROR_MESSAGE = "Empty response" +private const val NOT_DEFINED_ERROR_MESSAGE = "Not defined" +private const val UNAUTHORIZED_ERROR_MESSAGE = "Unauthorized" + +data class ErrorModel( + val message: String?, + val code: Int?, + var errorStatus: ErrorStatus +) { + constructor(errorStatus: ErrorStatus) : this(null, null, errorStatus) + + constructor( + message: String?, + errorStatus: ErrorStatus + ) : this(message, null, errorStatus) + + fun getErrorMessage(): String { + return when (errorStatus) { + ErrorStatus.NO_CONNECTION -> NO_CONNECTION_ERROR_MESSAGE + ErrorStatus.BAD_RESPONSE -> BAD_RESPONSE_ERROR_MESSAGE + ErrorStatus.TIMEOUT -> TIME_OUT_ERROR_MESSAGE + ErrorStatus.EMPTY_RESPONSE -> EMPTY_RESPONSE_ERROR_MESSAGE + ErrorStatus.NOT_DEFINED -> NOT_DEFINED_ERROR_MESSAGE + ErrorStatus.UNAUTHORIZED -> UNAUTHORIZED_ERROR_MESSAGE + } + } + + /** + * various error status to know what happened if something goes wrong with a repository call + */ + enum class ErrorStatus { + /** + * error in connecting to repository (Server or Database) + */ + NO_CONNECTION, + /** + * error in getting value (Json Error, Server Error, etc) + */ + BAD_RESPONSE, + /** + * Time out error + */ + TIMEOUT, + /** + * no data available in repository + */ + EMPTY_RESPONSE, + /** + * an unexpected error + */ + NOT_DEFINED, + /** + * bad credentials + */ + UNAUTHORIZED + } +} \ No newline at end of file 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 new file mode 100644 index 000000000..8ecadde7e --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/newarch/data/repository/NextcloudTalkRepositoryImpl.kt @@ -0,0 +1,34 @@ +/* + * 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.data.repository + +import com.nextcloud.talk.models.database.UserEntity +import com.nextcloud.talk.models.json.conversations.Conversation +import com.nextcloud.talk.newarch.data.source.remote.ApiService +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 { + 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/data/source/remote/ApiErrorHandler.kt b/app/src/main/java/com/nextcloud/talk/newarch/data/source/remote/ApiErrorHandler.kt new file mode 100644 index 000000000..67701aaa8 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/newarch/data/source/remote/ApiErrorHandler.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 . + * + * Inspired by https://github.com/ZahraHeydari/Android-Clean-Arch-Coroutines-Koin/blob + * /cfed12ecbfa425de53de505e76b3ec06c3bbdaca/app/src/main/java/com/android/post/data/source/remote/ApiErrorHandle.kt + */ + +package com.nextcloud.talk.newarch.data.source.remote + +import com.nextcloud.talk.newarch.data.model.ErrorModel +import okhttp3.ResponseBody +import retrofit2.HttpException +import java.io.IOException +import java.net.SocketTimeoutException + +/** + * This class trace exceptions(api call or parse data or connection errors) & + * depending on what exception returns a [ErrorModel] + * + * */ +class ApiErrorHandler { + + fun traceErrorException(throwable: Throwable?): ErrorModel { + val errorModel: ErrorModel? = when (throwable) { + + // if throwable is an instance of HttpException + // then attempt to parse error data from response body + is HttpException -> { + // handle UNAUTHORIZED situation (when token expired) + if (throwable.code() == 401) { + ErrorModel(throwable.message(), throwable.code(), ErrorModel.ErrorStatus.UNAUTHORIZED) + } else { + getHttpError(throwable.response()?.errorBody()) + } + } + + // handle api call timeout error + is SocketTimeoutException -> { + ErrorModel(throwable.message, ErrorModel.ErrorStatus.TIMEOUT) + } + + // handle connection error + is IOException -> { + ErrorModel(throwable.message, ErrorModel.ErrorStatus.NO_CONNECTION) + } + else -> null + } + return errorModel ?: ErrorModel("No Defined Error!", 0, ErrorModel.ErrorStatus.BAD_RESPONSE) + } + + /** + * attempts to parse http response body and get error data from it + * + * @param body retrofit response body + * @return returns an instance of [ErrorModel] with parsed data or NOT_DEFINED status + */ + private fun getHttpError(body: ResponseBody?): ErrorModel { + return try { + // use response body to get error detail + ErrorModel(body.toString(), 400, ErrorModel.ErrorStatus.BAD_RESPONSE) + } catch (e: Throwable) { + e.printStackTrace() + ErrorModel(message = e.message, errorStatus = ErrorModel.ErrorStatus.NOT_DEFINED) + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/newarch/data/source/remote/ApiService.kt b/app/src/main/java/com/nextcloud/talk/newarch/data/source/remote/ApiService.kt new file mode 100644 index 000000000..06efefc49 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/newarch/data/source/remote/ApiService.kt @@ -0,0 +1,39 @@ +/* + * 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.data.source.remote + +import com.nextcloud.talk.models.json.conversations.RoomsOverall +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.Url + +interface ApiService { + + /* + Server URL is: baseUrl + ocsApiVersion + spreedApiVersion + /room + */ + @GET + suspend fun getConversations( + @Header( + "Authorization" + ) authorization: String, @Url url: String + ): RoomsOverall +} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/newarch/di/module/CommunicationModule.kt b/app/src/main/java/com/nextcloud/talk/newarch/di/module/CommunicationModule.kt new file mode 100644 index 000000000..690220a9d --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/newarch/di/module/CommunicationModule.kt @@ -0,0 +1,32 @@ +/* + * 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.di.module + +import org.greenrobot.eventbus.EventBus +import org.koin.dsl.module + +val CommunicationModule = module { + single { createEventBus() } +} + +fun createEventBus() : EventBus { + return EventBus.getDefault(); +} \ 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 new file mode 100644 index 000000000..dd61b9c75 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/newarch/di/module/NetworkModule.kt @@ -0,0 +1,250 @@ +/* + * 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.di.module + +import android.content.Context +import android.text.TextUtils +import android.util.Log +import com.github.aurae.retrofit2.LoganSquareConverterFactory +import com.nextcloud.talk.BuildConfig +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.newarch.data.repository.NextcloudTalkRepositoryImpl +import com.nextcloud.talk.newarch.data.source.remote.ApiErrorHandler +import com.nextcloud.talk.newarch.data.source.remote.ApiService +import com.nextcloud.talk.newarch.domain.repository.NextcloudTalkRepository +import com.nextcloud.talk.newarch.domain.usecases.GetConversationsUseCase +import com.nextcloud.talk.newarch.utils.NetworkUtils +import com.nextcloud.talk.newarch.utils.NetworkUtils.GetProxyRunnable +import com.nextcloud.talk.newarch.utils.NetworkUtils.MagicAuthenticator +import com.nextcloud.talk.utils.LoggingUtils +import com.nextcloud.talk.utils.database.user.UserUtils +import com.nextcloud.talk.utils.preferences.AppPreferences +import com.nextcloud.talk.utils.ssl.MagicKeyManager +import com.nextcloud.talk.utils.ssl.MagicTrustManager +import com.nextcloud.talk.utils.ssl.SSLSocketFactoryCompat +import io.reactivex.schedulers.Schedulers +import okhttp3.Cache +import okhttp3.Credentials +import okhttp3.Dispatcher +import okhttp3.JavaNetCookieJar +import okhttp3.OkHttpClient +import okhttp3.internal.tls.OkHostnameVerifier +import okhttp3.logging.HttpLoggingInterceptor +import okhttp3.logging.HttpLoggingInterceptor.Logger +import org.koin.android.ext.koin.androidContext +import org.koin.dsl.module +import retrofit2.Retrofit +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory +import java.io.IOException +import java.net.CookieManager +import java.net.CookiePolicy.ACCEPT_NONE +import java.net.Proxy +import java.security.KeyStore +import java.security.KeyStoreException +import java.security.NoSuchAlgorithmException +import java.security.UnrecoverableKeyException +import java.security.cert.CertificateException +import java.util.concurrent.TimeUnit +import javax.net.ssl.KeyManagerFactory +import javax.net.ssl.X509KeyManager + +val NetworkModule = module { + single { createService(get()) } + single { createRetrofit(get()) } + single { createOkHttpClient(androidContext(), get(), get(), get(), get(), get(), get(), get()) } + factory { createGetConversationsUseCase(get(), get()) } +} + +fun createCookieManager(): CookieManager { + val cookieManager = CookieManager() + cookieManager.setCookiePolicy(ACCEPT_NONE) + return cookieManager +} + +fun createOkHttpClient( + context: Context, + proxy: Proxy, + appPreferences: AppPreferences, + magicTrustManager: MagicTrustManager, + sslSocketFactoryCompat: SSLSocketFactoryCompat, + cache: Cache, + cookieManager: CookieManager, + dispatcher: Dispatcher +): OkHttpClient { + val httpClient = OkHttpClient.Builder() + + httpClient.retryOnConnectionFailure(true) + httpClient.connectTimeout(45, TimeUnit.SECONDS) + httpClient.readTimeout(45, TimeUnit.SECONDS) + httpClient.writeTimeout(45, TimeUnit.SECONDS) + + httpClient.cookieJar(JavaNetCookieJar(cookieManager)) + httpClient.cache(cache) + + // Trust own CA and all self-signed certs + httpClient.sslSocketFactory(sslSocketFactoryCompat, magicTrustManager) + httpClient.retryOnConnectionFailure(true) + httpClient.hostnameVerifier(magicTrustManager.getHostnameVerifier(OkHostnameVerifier)) + + httpClient.dispatcher(dispatcher) + if (Proxy.NO_PROXY != proxy) { + httpClient.proxy(proxy) + + if (appPreferences.proxyCredentials && + !TextUtils.isEmpty(appPreferences.proxyUsername) && + !TextUtils.isEmpty(appPreferences.proxyPassword) + ) { + httpClient.proxyAuthenticator( + MagicAuthenticator( + Credentials.basic( + appPreferences.proxyUsername, + appPreferences.proxyPassword + ), "Proxy-Authorization" + ) + ) + } + } + + httpClient.addInterceptor(NetworkUtils.HeadersInterceptor()) + + if (BuildConfig.DEBUG && !context.getResources().getBoolean(R.bool.nc_is_debug)) { + val loggingInterceptor = HttpLoggingInterceptor() + loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY + loggingInterceptor.redactHeader("Authorization") + loggingInterceptor.redactHeader("Proxy-Authorization") + loggingInterceptor.redactHeader("Cookie") + httpClient.addInterceptor(loggingInterceptor) + } else if (context.getResources().getBoolean(R.bool.nc_is_debug)) { + + val fileLogger = HttpLoggingInterceptor(object : Logger { + override fun log(message: String) { + LoggingUtils.writeLogEntryToFile(context, message) + } + }) + + fileLogger.level = HttpLoggingInterceptor.Level.BODY + fileLogger.redactHeader("Authorization") + fileLogger.redactHeader("Proxy-Authorization") + fileLogger.redactHeader("Cookie") + httpClient.addInterceptor(fileLogger) + } + + return httpClient.build() + +} + +fun createRetrofit(okHttpClient: OkHttpClient): Retrofit { + return Retrofit.Builder() + .client(okHttpClient) + .baseUrl("https://nextcloud.com") + .addCallAdapterFactory(RxJava2CallAdapterFactory.createWithScheduler(Schedulers.io())) + .addConverterFactory(LoganSquareConverterFactory.create()) + .build() +} + +fun createTrustManager(): MagicTrustManager { + return MagicTrustManager(); +} + +fun createSslSocketFactory(magicKeyManager: MagicKeyManager, magicTrustManager: +MagicTrustManager) : SSLSocketFactoryCompat { + return SSLSocketFactoryCompat(magicKeyManager, magicTrustManager) +} + +fun createKeyManager( + appPreferences: AppPreferences, + userUtils: UserUtils +): MagicKeyManager? { + val keyStore: KeyStore? + try { + keyStore = KeyStore.getInstance("AndroidKeyStore") + keyStore.load(null) + val kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()) + kmf.init(keyStore, null) + val origKm = kmf.keyManagers[0] as X509KeyManager + return MagicKeyManager(origKm, userUtils, appPreferences) + } catch (e: KeyStoreException) { + Log.e("NetworkModule", "KeyStoreException " + e.localizedMessage!!) + } catch (e: CertificateException) { + Log.e("NetworkModule", "CertificateException " + e.localizedMessage!!) + } catch (e: NoSuchAlgorithmException) { + Log.e("NetworkModule", "NoSuchAlgorithmException " + e.localizedMessage!!) + } catch (e: IOException) { + Log.e("NetworkModule", "IOException " + e.localizedMessage!!) + } catch (e: UnrecoverableKeyException) { + Log.e("NetworkModule", "UnrecoverableKeyException " + e.localizedMessage!!) + } + + return null +} + +fun createProxy(appPreferences: AppPreferences): Proxy { + if (!TextUtils.isEmpty(appPreferences.proxyType) && "No proxy" != appPreferences.proxyType + && !TextUtils.isEmpty(appPreferences.proxyHost) + ) { + val getProxyRunnable = GetProxyRunnable(appPreferences) + val getProxyThread = Thread(getProxyRunnable) + getProxyThread.start() + try { + getProxyThread.join() + return getProxyRunnable.proxyValue + } catch (e: InterruptedException) { + Log.e("NetworkModule", "Failed to join the thread while getting proxy: " + e.localizedMessage) + return Proxy.NO_PROXY + } + + } else { + return Proxy.NO_PROXY + } + +} + +fun createDispatcher() : Dispatcher { + val dispatcher = Dispatcher() + dispatcher.maxRequestsPerHost = 100 + dispatcher.maxRequests = 100 + return dispatcher +} + +fun createCache(androidApplication: NextcloudTalkApplication) : Cache { + val cacheSize = 128 * 1024 * 1024 // 128 MB + return Cache(androidApplication.cacheDir, cacheSize.toLong()) +} + +fun createApiErrorHandler(): ApiErrorHandler { + return ApiErrorHandler() +} + +fun createService(retrofit: Retrofit): ApiService { + return retrofit.create(ApiService::class.java) +} + +fun createNextcloudTalkRepository(apiService: ApiService): NextcloudTalkRepository { + 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/di/module/StorageModule.kt b/app/src/main/java/com/nextcloud/talk/newarch/di/module/StorageModule.kt new file mode 100644 index 000000000..94277a9cc --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/newarch/di/module/StorageModule.kt @@ -0,0 +1,62 @@ +/* + * 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.di.module + +import android.content.Context +import com.nextcloud.talk.R.string +import com.nextcloud.talk.models.database.Models +import com.nextcloud.talk.utils.database.user.UserUtils +import com.nextcloud.talk.utils.preferences.AppPreferences +import io.requery.Persistable +import io.requery.android.sqlcipher.SqlCipherDatabaseSource +import io.requery.reactivex.ReactiveEntityStore +import io.requery.reactivex.ReactiveSupport +import io.requery.sql.EntityDataStore +import net.orange_box.storebox.StoreBox +import org.koin.android.ext.koin.androidContext +import org.koin.dsl.module + +val StorageModule = module { + single { createPreferences(androidContext()) } + single { createSqlCipherDatabaseSource(androidContext()) } + single { createDataStore(get()) } + single { createUserUtils(get()) } +} + +fun createPreferences(context: Context): AppPreferences { + return StoreBox.create(context, AppPreferences::class.java) +} + +fun createSqlCipherDatabaseSource(context: Context): SqlCipherDatabaseSource { + return SqlCipherDatabaseSource(context, Models.DEFAULT, + context.resources.getString(string.nc_app_name).toLowerCase() + .replace(" ", "_").trim { it <= ' ' } + ".sqlite", + context.getString(string.nc_talk_database_encryption_key), 6) +} + +fun createDataStore(sqlCipherDatabaseSource: SqlCipherDatabaseSource): ReactiveEntityStore { + val configuration = sqlCipherDatabaseSource.configuration + return ReactiveSupport.toReactiveStore(EntityDataStore(configuration)) +} + +fun createUserUtils(dataStore : ReactiveEntityStore) : UserUtils { + return UserUtils(dataStore); +} \ No newline at end of file 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 new file mode 100644 index 000000000..6241f7c32 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/newarch/domain/repository/NextcloudTalkRepository.kt @@ -0,0 +1,28 @@ +/* + * 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.domain.repository + +import com.nextcloud.talk.models.database.UserEntity +import com.nextcloud.talk.models.json.conversations.Conversation + +interface NextcloudTalkRepository { + suspend fun getConversations(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 new file mode 100644 index 000000000..b8bdec069 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/newarch/domain/usecases/GetConversationsUseCase.kt @@ -0,0 +1,36 @@ +/* + * 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.domain.usecases + +import com.nextcloud.talk.models.json.conversations.Conversation +import com.nextcloud.talk.newarch.data.source.remote.ApiErrorHandler +import com.nextcloud.talk.newarch.domain.repository.NextcloudTalkRepository +import com.nextcloud.talk.newarch.domain.usecases.base.UseCase + +class GetConversationsUseCase constructor( + private val nextcloudTalkRepository: NextcloudTalkRepository, + apiErrorHandler: ApiErrorHandler? +) : UseCase, Any?>(apiErrorHandler) { + + override suspend fun run(params: Any?): List { + return nextcloudTalkRepository.getConversations(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 new file mode 100644 index 000000000..4df78277c --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/newarch/domain/usecases/base/BaseUseCase.kt @@ -0,0 +1,51 @@ +/* + * 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.domain.usecases.base + +import com.nextcloud.talk.models.database.UserEntity +import com.nextcloud.talk.newarch.data.source.remote.ApiErrorHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.async +import kotlinx.coroutines.launch +import retrofit2.HttpException + +abstract class UseCase(private val apiErrorHandler: ApiErrorHandler?) where Type : Any { + + abstract suspend fun run(params: Params? = null): Type + lateinit var user: UserEntity + + fun invoke( + scope: CoroutineScope, + params: Params?, + onResult: (UseCaseResponse) + ) { + val backgroundJob = scope.async { run(params) } + scope.launch { + backgroundJob.await().let { + try { + onResult.onSuccess(it) + } catch (e: HttpException) { + onResult.onError(apiErrorHandler?.traceErrorException(e)) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/newarch/domain/usecases/base/UseCaseResponse.kt b/app/src/main/java/com/nextcloud/talk/newarch/domain/usecases/base/UseCaseResponse.kt new file mode 100644 index 000000000..2c33f963f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/newarch/domain/usecases/base/UseCaseResponse.kt @@ -0,0 +1,29 @@ +/* + * 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.domain.usecases.base + +import com.nextcloud.talk.newarch.data.model.ErrorModel + +interface UseCaseResponse { + fun onSuccess(result: Type) + fun onError(errorModel: ErrorModel?) +} + diff --git a/app/src/main/java/com/nextcloud/talk/newarch/utils/NetworkUtils.kt b/app/src/main/java/com/nextcloud/talk/newarch/utils/NetworkUtils.kt new file mode 100644 index 000000000..5bef4296f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/newarch/utils/NetworkUtils.kt @@ -0,0 +1,118 @@ +/* + * 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.utils + +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.preferences.AppPreferences +import com.nextcloud.talk.utils.singletons.AvatarStatusCodeHolder +import okhttp3.Authenticator +import okhttp3.Interceptor +import okhttp3.Request +import okhttp3.Response +import okhttp3.Route +import java.io.IOException +import java.net.InetSocketAddress +import java.net.Proxy +import java.net.Proxy.NO_PROXY +import java.net.Proxy.Type +import java.net.Proxy.Type.SOCKS + +class NetworkUtils { + class HeadersInterceptor : Interceptor { + + @Throws(IOException::class) + override fun intercept(chain: Interceptor.Chain): Response { + val original = chain.request() + val request = original.newBuilder() + .header("User-Agent", ApiUtils.getUserAgent()) + .header("Accept", "application/json") + .header("OCS-APIRequest", "true") + .method(original.method, original.body) + .build() + + val response = chain.proceed(request) + + if (request.url.encodedPath.contains("/avatar/")) { + AvatarStatusCodeHolder.getInstance() + .statusCode = response.code + } + + return response + } + } + + class MagicAuthenticator( + private val credentials: String, + private val authenticatorType: String + ) : Authenticator { + + override fun authenticate( + route: Route?, + response: Response + ): Request? { + if (response.request.header(authenticatorType) != null) { + return null + } + + var countedResponse: Response? = response + + var attemptsCount = 0 + + + while ({ countedResponse = countedResponse?.priorResponse; countedResponse }() != null) { + attemptsCount++ + if (attemptsCount == 3) { + return null + } + } + return response.request.newBuilder() + .header(authenticatorType, credentials) + .build() + } + } + + class GetProxyRunnable internal constructor(private val appPreferences: AppPreferences) : + Runnable { + @Volatile internal var proxyValue: Proxy = NO_PROXY + private set + + override fun run() { + if (SOCKS == Type.valueOf(appPreferences.proxyType)) { + proxyValue = Proxy( + Type.valueOf(appPreferences.proxyType), + InetSocketAddress.createUnresolved( + appPreferences.proxyHost, Integer.parseInt( + appPreferences.proxyPort + ) + ) + ) + } else { + proxyValue = Proxy( + Type.valueOf(appPreferences.proxyType), + InetSocketAddress( + appPreferences.proxyHost, + Integer.parseInt(appPreferences.proxyPort) + ) + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/utils/database/user/UserUtils.java b/app/src/main/java/com/nextcloud/talk/utils/database/user/UserUtils.java index 295b5bbd9..1fbffc998 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/database/user/UserUtils.java +++ b/app/src/main/java/com/nextcloud/talk/utils/database/user/UserUtils.java @@ -36,7 +36,7 @@ import java.util.List; public class UserUtils { private ReactiveEntityStore dataStore; - UserUtils(ReactiveEntityStore dataStore) { + public UserUtils(ReactiveEntityStore dataStore) { this.dataStore = dataStore; }