From ca4998614cadad20c35b5f3cff4cbe29e9a7dc8c Mon Sep 17 00:00:00 2001 From: Mario Danic Date: Thu, 31 Oct 2019 02:05:25 +0100 Subject: [PATCH] Compiles again Signed-off-by: Mario Danic --- .../1.json | 52 +- .../talk/activities/MagicCallActivity.kt | 2 +- .../talk/adapters/items/ConversationItem.kt | 15 +- .../nextcloud/talk/adapters/items/UserItem.kt | 3 +- .../MagicIncomingTextMessageViewHolder.java | 207 -- .../MagicIncomingTextMessageViewHolder.kt | 207 ++ .../MagicOutcomingTextMessageViewHolder.java | 4 +- .../messages/MagicPreviewMessageViewHolder.kt | 8 +- .../application/NextcloudTalkApplication.kt | 22 +- .../MentionAutocompleteCallback.java | 5 +- .../adapters/items/BrowserFileItem.kt | 10 +- .../controllers/BrowserController.java | 5 +- .../filebrowser/operations/DavListing.java | 3 +- .../operations/ListingAbstractClass.java | 3 +- .../webdav/ReadFilesystemOperation.java | 3 +- .../talk/controllers/CallController.java | 2400 ---------------- .../talk/controllers/CallController.kt | 2478 +++++++++++++++++ .../talk/controllers/ChatController.kt | 36 +- .../talk/controllers/ContactsController.java | 1051 ------- .../talk/controllers/ContactsController.kt | 1041 +++++++ .../controllers/ConversationInfoController.kt | 7 +- .../talk/events/BottomSheetLockEvent.java | 10 +- .../talk/events/MediaStreamEvent.java | 6 +- .../nextcloud/talk/events/NetworkEvent.java | 2 +- .../talk/events/PeerConnectionEvent.java | 10 +- .../events/SessionDescriptionSendEvent.java | 10 +- .../nextcloud/talk/jobs/NotificationWorker.kt | 33 +- .../talk/jobs/PushRegistrationWorker.java | 45 - .../talk/jobs/PushRegistrationWorker.kt | 49 + .../talk/jobs/WebsocketConnectionsWorker.java | 87 - .../talk/jobs/WebsocketConnectionsWorker.kt | 80 + ...Server.java => ExternalSignalingServer.kt} | 25 +- .../talk/models/SignatureVerification.java | 3 +- .../json/autocomplete/AutocompleteOCS.java | 2 +- .../autocomplete/AutocompleteOverall.java | 2 +- .../json/autocomplete/AutocompleteUser.java | 6 +- .../json/capabilities/CapabilitiesList.java | 2 +- .../json/capabilities/CapabilitiesOCS.java | 2 +- .../capabilities/CapabilitiesOverall.java | 2 +- .../json/capabilities/SpreedCapability.java | 4 +- .../talk/models/json/chat/ChatMessage.java | 7 +- .../models/json/conversations/Conversation.kt | 14 +- ...onState.java => PushConfigurationState.kt} | 41 +- .../models/json/push/PushRegistration.java | 6 +- .../models/json/push/PushRegistrationOCS.java | 2 +- .../json/push/PushRegistrationOverall.java | 2 +- .../signaling/DataChannelMessageNick.java | 4 +- .../models/json/signaling/NCIceCandidate.java | 6 +- .../json/signaling/NCMessagePayload.java | 10 +- .../json/signaling/NCMessageWrapper.java | 6 +- .../json/signaling/NCSignalingMessage.java | 14 +- .../talk/models/json/signaling/Signaling.java | 4 +- .../models/json/signaling/SignalingOCS.java | 2 +- .../json/signaling/SignalingOverall.java | 2 +- .../json/signaling/settings/IceServer.java | 8 +- .../json/signaling/settings/Settings.java | 8 +- .../settings/SignalingSettingsOcs.java | 2 +- .../settings/SignalingSettingsOverall.java | 2 +- .../repository/offline/UsersRepositoryImpl.kt | 12 + .../online/NextcloudTalkRepositoryImpl.kt | 10 +- .../repository/offline/UsersRepository.kt | 7 +- .../online/NextcloudTalkRepository.kt | 9 +- .../ConversationListViewModelFactory.kt | 6 +- .../ConversationsListView.kt | 9 +- .../ConversationsListViewModel.kt | 43 +- .../di/module/ConversationsListModule.kt | 8 +- .../talk/newarch/local/dao/UsersDao.kt | 11 + .../local/models/ConversationEntity.kt | 10 +- .../newarch/local/models/MessageEntity.kt | 17 +- .../talk/newarch/local/models/UserNgEntity.kt | 66 +- .../talk/newarch/utils/Extensions.kt | 1 + .../nextcloud/talk/newarch/utils/Images.kt | 4 +- .../com/nextcloud/talk/utils/DisplayUtils.kt | 5 +- .../nextcloud/talk/utils/NotificationUtils.kt | 9 +- .../com/nextcloud/talk/utils/PushUtils.java | 445 --- .../com/nextcloud/talk/utils/PushUtils.kt | 450 +++ .../DatabaseStorageFactory.java | 5 +- .../DatabaseStorageModule.java | 297 -- .../DatabaseStorageModule.kt | 280 ++ .../ApplicationWideCurrentRoomHolder.java | 85 - .../talk/webrtc/MagicWebSocketInstance.java | 5 +- .../webrtc/WebSocketConnectionHelper.java | 5 +- 82 files changed, 4966 insertions(+), 4915 deletions(-) delete mode 100644 app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.java create mode 100644 app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.kt delete mode 100644 app/src/main/java/com/nextcloud/talk/controllers/CallController.java create mode 100644 app/src/main/java/com/nextcloud/talk/controllers/CallController.kt delete mode 100644 app/src/main/java/com/nextcloud/talk/controllers/ContactsController.java create mode 100644 app/src/main/java/com/nextcloud/talk/controllers/ContactsController.kt delete mode 100644 app/src/main/java/com/nextcloud/talk/jobs/PushRegistrationWorker.java create mode 100644 app/src/main/java/com/nextcloud/talk/jobs/PushRegistrationWorker.kt delete mode 100644 app/src/main/java/com/nextcloud/talk/jobs/WebsocketConnectionsWorker.java create mode 100644 app/src/main/java/com/nextcloud/talk/jobs/WebsocketConnectionsWorker.kt rename app/src/main/java/com/nextcloud/talk/models/{ExternalSignalingServer.java => ExternalSignalingServer.kt} (60%) rename app/src/main/java/com/nextcloud/talk/models/json/push/{PushConfigurationState.java => PushConfigurationState.kt} (51%) delete mode 100644 app/src/main/java/com/nextcloud/talk/utils/PushUtils.java create mode 100644 app/src/main/java/com/nextcloud/talk/utils/PushUtils.kt delete mode 100644 app/src/main/java/com/nextcloud/talk/utils/preferences/preferencestorage/DatabaseStorageModule.java create mode 100644 app/src/main/java/com/nextcloud/talk/utils/preferences/preferencestorage/DatabaseStorageModule.kt delete mode 100644 app/src/main/java/com/nextcloud/talk/utils/singletons/ApplicationWideCurrentRoomHolder.java diff --git a/app/schemas/com.nextcloud.talk.newarch.local.db.TalkDatabase/1.json b/app/schemas/com.nextcloud.talk.newarch.local.db.TalkDatabase/1.json index 01f1f0abb..34250b1ae 100644 --- a/app/schemas/com.nextcloud.talk.newarch.local.db.TalkDatabase/1.json +++ b/app/schemas/com.nextcloud.talk.newarch.local.db.TalkDatabase/1.json @@ -2,11 +2,11 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "8b9e5dddd027e51eb17ffd53d365e6d4", + "identityHash": "c81b4edb8abdd29b77836d16a7d991c2", "entities": [ { "tableName": "conversations", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `user` INTEGER, `conversation_id` TEXT, `token` TEXT, `name` TEXT, `display_name` TEXT, `type` INTEGER, `count` INTEGER NOT NULL, `number_of_guests` INTEGER NOT NULL, `participants_count` INTEGER NOT NULL, `participant_type` INTEGER, `has_password` INTEGER NOT NULL, `session_id` TEXT, `favorite` INTEGER NOT NULL, `last_activity` INTEGER NOT NULL, `unread_messages` INTEGER NOT NULL, `unread_mention` INTEGER NOT NULL, `last_message` TEXT, `object_type` TEXT, `notification_level` INTEGER, `read_only_state` INTEGER, `lobby_state` INTEGER, `lobby_timer` INTEGER, `last_read_message_id` INTEGER NOT NULL, `modified_at` INTEGER, `changing` INTEGER NOT NULL, FOREIGN KEY(`user`) REFERENCES `users`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `user` INTEGER, `conversation_id` TEXT, `token` TEXT, `name` TEXT, `display_name` TEXT, `type` INTEGER, `count` INTEGER NOT NULL, `number_of_guests` INTEGER NOT NULL, `participants_count` INTEGER NOT NULL, `participant_type` INTEGER, `has_password` INTEGER NOT NULL, `session_id` TEXT, `favorite` INTEGER NOT NULL, `last_activity` INTEGER NOT NULL, `unread_messages` INTEGER NOT NULL, `unread_mention` INTEGER NOT NULL, `last_message` TEXT, `object_type` TEXT, `notification_level` INTEGER, `read_only_state` INTEGER, `lobby_state` INTEGER, `lobby_timer` INTEGER, `last_read_message` INTEGER NOT NULL, `modified_at` INTEGER, `changing` INTEGER NOT NULL, FOREIGN KEY(`user`) REFERENCES `users`(`id`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", "fields": [ { "fieldPath": "id", @@ -148,7 +148,7 @@ }, { "fieldPath": "lastReadMessageId", - "columnName": "last_read_message_id", + "columnName": "last_read_message", "affinity": "INTEGER", "notNull": true }, @@ -173,19 +173,20 @@ }, "indices": [ { - "name": "index_conversations_user", - "unique": false, + "name": "index_conversations_user_conversation_id", + "unique": true, "columnNames": [ - "user" + "user", + "conversation_id" ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_conversations_user` ON `${TABLE_NAME}` (`user`)" + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_conversations_user_conversation_id` ON `${TABLE_NAME}` (`user`, `conversation_id`)" } ], "foreignKeys": [ { "table": "users", "onDelete": "CASCADE", - "onUpdate": "NO ACTION", + "onUpdate": "CASCADE", "columns": [ "user" ], @@ -197,7 +198,7 @@ }, { "tableName": "messages", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `user` INTEGER, `conversation` INTEGER, `message_id` INTEGER NOT NULL, `actor_id` TEXT, `actor_type` TEXT, `actor_display_name` TEXT, `timestamp` INTEGER NOT NULL, `message` TEXT, `system_message_type` TEXT, FOREIGN KEY(`conversation`) REFERENCES `conversations`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `conversation` INTEGER, `message_id` INTEGER NOT NULL, `actor_id` TEXT, `actor_type` TEXT, `actor_display_name` TEXT, `timestamp` INTEGER NOT NULL, `message` TEXT, `system_message_type` TEXT, FOREIGN KEY(`conversation`) REFERENCES `conversations`(`id`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", "fields": [ { "fieldPath": "id", @@ -205,12 +206,6 @@ "affinity": "INTEGER", "notNull": false }, - { - "fieldPath": "user", - "columnName": "user", - "affinity": "INTEGER", - "notNull": false - }, { "fieldPath": "conversation", "columnName": "conversation", @@ -274,22 +269,13 @@ "conversation" ], "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_conversation` ON `${TABLE_NAME}` (`conversation`)" - }, - { - "name": "index_messages_user_conversation", - "unique": false, - "columnNames": [ - "user", - "conversation" - ], - "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_user_conversation` ON `${TABLE_NAME}` (`user`, `conversation`)" } ], "foreignKeys": [ { "table": "conversations", "onDelete": "CASCADE", - "onUpdate": "NO ACTION", + "onUpdate": "CASCADE", "columns": [ "conversation" ], @@ -301,25 +287,31 @@ }, { "tableName": "users", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `user_id` TEXT, `username` TEXT, `token` TEXT, `display_name` TEXT, `push_configuration` TEXT, `capabilities` TEXT, `client_auth_cert` TEXT, `external_signaling` TEXT, `status` INTEGER)", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `user_id` TEXT NOT NULL, `username` TEXT NOT NULL, `base_url` TEXT NOT NULL, `token` TEXT, `display_name` TEXT, `push_configuration` TEXT, `capabilities` TEXT, `client_auth_cert` TEXT, `external_signaling` TEXT, `status` INTEGER)", "fields": [ { "fieldPath": "id", "columnName": "id", "affinity": "INTEGER", - "notNull": false + "notNull": true }, { "fieldPath": "userId", "columnName": "user_id", "affinity": "TEXT", - "notNull": false + "notNull": true }, { "fieldPath": "username", "columnName": "username", "affinity": "TEXT", - "notNull": false + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "base_url", + "affinity": "TEXT", + "notNull": true }, { "fieldPath": "token", @@ -377,7 +369,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8b9e5dddd027e51eb17ffd53d365e6d4')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c81b4edb8abdd29b77836d16a7d991c2')" ] } } \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/activities/MagicCallActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/MagicCallActivity.kt index a76854c10..3b7f15836 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/MagicCallActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/MagicCallActivity.kt @@ -76,7 +76,7 @@ class MagicCallActivity : BaseActivity() { ) } else { router!!.setRoot( - RouterTransaction.with(CallController(intent.extras)) + RouterTransaction.with(CallController(intent.extras!!)) .pushChangeHandler(HorizontalChangeHandler()) .popChangeHandler(HorizontalChangeHandler()) ) diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.kt b/app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.kt index 5ffa253ea..c7c4ba2b7 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.kt @@ -34,6 +34,7 @@ import com.nextcloud.talk.models.database.UserEntity import com.nextcloud.talk.models.json.chat.ChatMessage import com.nextcloud.talk.models.json.conversations.Conversation import com.nextcloud.talk.models.json.conversations.Conversation.ConversationType.ONE_TO_ONE_CONVERSATION +import com.nextcloud.talk.newarch.local.models.UserNgEntity import com.nextcloud.talk.utils.ApiUtils import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.AbstractFlexibleItem @@ -54,7 +55,7 @@ import java.util.regex.Pattern class ConversationItem( val model: Conversation, - private val userEntity: UserEntity, + private val user: UserNgEntity, private val context: Context ) : AbstractFlexibleItem(), IFilterable { @@ -74,7 +75,7 @@ class ConversationItem( && model.unreadMention == comparedConversation.unreadMention && model.objectType == comparedConversation.objectType && model.changing == comparedConversation.changing - && userEntity.id == inItem.userEntity.id) + && user.id == inItem.user.id) } return false } @@ -82,7 +83,7 @@ class ConversationItem( override fun hashCode(): Int { return Objects.hash( model.conversationId, model.token, - userEntity.id + user.id ) } @@ -168,13 +169,13 @@ class ConversationItem( holder.itemView.dialogLastMessage!!.text = model.lastMessage!!.text } else { var authorDisplayName = "" - model.lastMessage!!.activeUser = userEntity + model.lastMessage!!.activeUser = user val text: String if (model.lastMessage!! .messageType == ChatMessage.MessageType.REGULAR_TEXT_MESSAGE && (!(ONE_TO_ONE_CONVERSATION).equals( - model.type) || model.lastMessage!!.actorId == userEntity.userId) + model.type) || model.lastMessage!!.actorId == user.userId) ) { - if (model.lastMessage!!.actorId == userEntity.userId) { + if (model.lastMessage!!.actorId == user.userId) { text = String.format( appContext.getString(R.string.nc_formatted_message_you), model.lastMessage!!.lastMessageDisplayText @@ -248,7 +249,7 @@ class ConversationItem( ) { holder.itemView.dialogAvatar.load( ApiUtils.getUrlForAvatarWithName( - userEntity.baseUrl, + user.baseUrl, model.name, R.dimen.avatar_size ) ) { diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/UserItem.kt b/app/src/main/java/com/nextcloud/talk/adapters/items/UserItem.kt index 222fc6125..9755857bc 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/items/UserItem.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/items/UserItem.kt @@ -35,6 +35,7 @@ import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.models.database.UserEntity import com.nextcloud.talk.models.json.converters.EnumParticipantTypeConverter import com.nextcloud.talk.models.json.participants.Participant +import com.nextcloud.talk.newarch.local.models.UserNgEntity import com.nextcloud.talk.utils.ApiUtils import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.AbstractFlexibleItem @@ -51,7 +52,7 @@ class UserItem( */ val model: Participant, - val entity: UserEntity, + val entity: UserNgEntity, private var header: GenericTextHeaderItem?, private val activityContext: Context ) : AbstractFlexibleItem(), diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.java b/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.java deleted file mode 100644 index 07cbcfdc4..000000000 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.java +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Nextcloud Talk application - * - * @author Mario Danic - * Copyright (C) 2017-2018 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.adapters.messages; - -import android.content.Context; -import android.content.Intent; -import android.content.res.Resources; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.LayerDrawable; -import android.net.Uri; -import android.text.Spannable; -import android.text.SpannableString; -import android.text.TextUtils; -import android.util.TypedValue; -import android.view.View; -import android.widget.TextView; -import androidx.core.view.ViewCompat; -import androidx.emoji.widget.EmojiTextView; -import autodagger.AutoInjector; -import butterknife.BindView; -import butterknife.ButterKnife; -import com.amulyakhare.textdrawable.TextDrawable; -import com.facebook.drawee.view.SimpleDraweeView; -import com.google.android.flexbox.FlexboxLayout; -import com.nextcloud.talk.R; -import com.nextcloud.talk.application.NextcloudTalkApplication; -import com.nextcloud.talk.models.json.chat.ChatMessage; -import com.nextcloud.talk.utils.DisplayUtils; -import com.nextcloud.talk.utils.TextMatchers; -import com.nextcloud.talk.utils.database.user.UserUtils; -import com.nextcloud.talk.utils.preferences.AppPreferences; -import com.stfalcon.chatkit.messages.MessageHolders; -import java.util.HashMap; -import java.util.Map; -import javax.inject.Inject; - -@AutoInjector(NextcloudTalkApplication.class) -public class MagicIncomingTextMessageViewHolder - extends MessageHolders.IncomingTextMessageViewHolder { - - @BindView(R.id.messageAuthor) - EmojiTextView messageAuthor; - - @BindView(R.id.messageText) - EmojiTextView messageText; - - @BindView(R.id.messageUserAvatar) - SimpleDraweeView messageUserAvatarView; - - @BindView(R.id.messageTime) - TextView messageTimeView; - - @Inject - UserUtils userUtils; - - @Inject - Context context; - - @Inject - AppPreferences appPreferences; - - private View itemView; - - public MagicIncomingTextMessageViewHolder(View itemView) { - super(itemView); - ButterKnife.bind(this, itemView); - NextcloudTalkApplication.Companion.getSharedApplication() - .getComponentApplication() - .inject(this); - - this.itemView = itemView; - } - - @Override - public void onBind(ChatMessage message) { - super.onBind(message); - String author; - - if (!TextUtils.isEmpty(author = message.getActorDisplayName())) { - messageAuthor.setText(author); - } else { - messageAuthor.setText(R.string.nc_nick_guest); - } - - if (!message.isGrouped() && !message.isOneToOneConversation()) { - messageUserAvatarView.setVisibility(View.VISIBLE); - if (message.getActorType().equals("guests")) { - // do nothing, avatar is set - } else if (message.getActorType().equals("bots") && message.getActorId() - .equals("changelog")) { - messageUserAvatarView.setController(null); - Drawable[] layers = new Drawable[2]; - layers[0] = context.getDrawable(R.drawable.ic_launcher_background); - layers[1] = context.getDrawable(R.drawable.ic_launcher_foreground); - LayerDrawable layerDrawable = new LayerDrawable(layers); - - messageUserAvatarView.getHierarchy() - .setPlaceholderImage(DisplayUtils.INSTANCE.getRoundedDrawable(layerDrawable)); - } else if (message.getActorType().equals("bots")) { - messageUserAvatarView.setController(null); - TextDrawable drawable = - TextDrawable.builder().beginConfig().bold().endConfig().buildRound(">", - context.getResources().getColor(R.color.black)); - messageUserAvatarView.setVisibility(View.VISIBLE); - messageUserAvatarView.getHierarchy().setPlaceholderImage(drawable); - } - } else { - if (message.isOneToOneConversation()) { - messageUserAvatarView.setVisibility(View.GONE); - } else { - messageUserAvatarView.setVisibility(View.INVISIBLE); - } - messageAuthor.setVisibility(View.GONE); - } - - Resources resources = itemView.getResources(); - - int bg_bubble_color = resources.getColor(R.color.bg_message_list_incoming_bubble); - - int bubbleResource = R.drawable.shape_incoming_message; - - if (message.isGrouped) { - bubbleResource = R.drawable.shape_grouped_incoming_message; - } - - Drawable bubbleDrawable = DisplayUtils.INSTANCE.getMessageSelector(bg_bubble_color, - resources.getColor(R.color.transparent), - bg_bubble_color, bubbleResource); - ViewCompat.setBackground(bubble, bubbleDrawable); - - HashMap> messageParameters = message.getMessageParameters(); - - itemView.setSelected(false); - messageTimeView.setTextColor(context.getResources().getColor(R.color.warm_grey_four)); - - FlexboxLayout.LayoutParams layoutParams = - (FlexboxLayout.LayoutParams) messageTimeView.getLayoutParams(); - layoutParams.setWrapBefore(false); - - Spannable messageString = new SpannableString(message.getText()); - - float textSize = context.getResources().getDimension(R.dimen.chat_text_size); - - if (messageParameters != null && messageParameters.size() > 0) { - for (String key : messageParameters.keySet()) { - Map individualHashMap = message.getMessageParameters().get(key); - if (individualHashMap != null) { - if (individualHashMap.get("type").equals("user") || individualHashMap.get("type") - .equals("guest") || individualHashMap.get("type").equals("call")) { - if (individualHashMap.get("id").equals(message.getActiveUser().getUserId())) { - messageString = - DisplayUtils.INSTANCE.searchAndReplaceWithMentionSpan(messageText.getContext(), - messageString, - individualHashMap.get("id"), - individualHashMap.get("name"), - individualHashMap.get("type"), - userUtils.getUserById(message.getActiveUser().getUserId()), - R.xml.chip_you); - } else { - messageString = - DisplayUtils.INSTANCE.searchAndReplaceWithMentionSpan(messageText.getContext(), - messageString, - individualHashMap.get("id"), - individualHashMap.get("name"), - individualHashMap.get("type"), - userUtils.getUserById(message.getActiveUser().getUserId()), - R.xml.chip_others); - } - } else if (individualHashMap.get("type").equals("file")) { - itemView.setOnClickListener(v -> { - Intent browserIntent = - new Intent(Intent.ACTION_VIEW, Uri.parse(individualHashMap.get("link"))); - context.startActivity(browserIntent); - }); - } - } - } - } else if (TextMatchers.isMessageWithSingleEmoticonOnly(message.getText())) { - textSize = (float) (textSize * 2.5); - layoutParams.setWrapBefore(true); - itemView.setSelected(true); - messageAuthor.setVisibility(View.GONE); - } - - messageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); - messageTimeView.setLayoutParams(layoutParams); - messageText.setText(messageString); - } -} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.kt new file mode 100644 index 000000000..7276260c3 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.kt @@ -0,0 +1,207 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * Copyright (C) 2017-2018 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.adapters.messages + +import android.content.Context +import android.content.Intent +import android.graphics.drawable.Drawable +import android.graphics.drawable.LayerDrawable +import android.net.Uri +import android.text.Spannable +import android.text.SpannableString +import android.text.TextUtils +import android.util.TypedValue +import android.view.View +import android.widget.TextView +import androidx.core.view.ViewCompat +import androidx.emoji.widget.EmojiTextView +import autodagger.AutoInjector +import butterknife.BindView +import butterknife.ButterKnife +import com.amulyakhare.textdrawable.TextDrawable +import com.facebook.drawee.view.SimpleDraweeView +import com.google.android.flexbox.FlexboxLayout +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.models.json.chat.ChatMessage +import com.nextcloud.talk.utils.DisplayUtils +import com.nextcloud.talk.utils.TextMatchers +import com.nextcloud.talk.utils.preferences.AppPreferences +import com.stfalcon.chatkit.messages.MessageHolders +import org.koin.core.KoinComponent +import org.koin.core.inject +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class MagicIncomingTextMessageViewHolder(incomingView: View) : MessageHolders +.IncomingTextMessageViewHolder(incomingView), KoinComponent { + + @JvmField + @BindView(R.id.messageAuthor) + var messageAuthor: EmojiTextView? = null + + @JvmField + @BindView(R.id.messageText) + var messageText: EmojiTextView? = null + + @JvmField + @BindView(R.id.messageUserAvatar) + var messageUserAvatarView: SimpleDraweeView? = null + + @JvmField + @BindView(R.id.messageTime) + var messageTimeView: TextView? = null + + @JvmField + @Inject + var context: Context? = null + + val appPreferences: AppPreferences by inject() + + init { + ButterKnife.bind( + this, + itemView + ) + NextcloudTalkApplication.sharedApplication!! + .componentApplication + .inject(this) + } + + override fun onBind(message: ChatMessage) { + super.onBind(message) + val author: String = message.actorDisplayName + if (!TextUtils.isEmpty(author)) { + messageAuthor!!.text = author + } else { + messageAuthor!!.setText(R.string.nc_nick_guest) + } + + if (!message.grouped && !message.oneToOneConversation) { + messageUserAvatarView!!.visibility = View.VISIBLE + if (message.actorType == "guests") { + // do nothing, avatar is set + } else if (message.actorType == "bots" && message.actorType == "changelog") { + messageUserAvatarView!!.controller = null + val layers = arrayOfNulls(2) + layers[0] = context!!.getDrawable(R.drawable.ic_launcher_background) + layers[1] = context!!.getDrawable(R.drawable.ic_launcher_foreground) + val layerDrawable = LayerDrawable(layers) + + messageUserAvatarView!!.hierarchy + .setPlaceholderImage(DisplayUtils.getRoundedDrawable(layerDrawable)) + } else if (message.actorType == "bots") { + messageUserAvatarView!!.controller = null + val drawable = TextDrawable.builder() + .beginConfig() + .bold() + .endConfig() + .buildRound( + ">", + context!!.resources.getColor(R.color.black) + ) + messageUserAvatarView!!.visibility = View.VISIBLE + messageUserAvatarView!!.hierarchy.setPlaceholderImage(drawable) + } + } else { + if (message.oneToOneConversation) { + messageUserAvatarView!!.visibility = View.GONE + } else { + messageUserAvatarView!!.visibility = View.INVISIBLE + } + messageAuthor!!.visibility = View.GONE + } + + val resources = itemView.getResources() + + val bg_bubble_color = resources.getColor(R.color.bg_message_list_incoming_bubble) + + var bubbleResource = R.drawable.shape_incoming_message + + if (message.grouped) { + bubbleResource = R.drawable.shape_grouped_incoming_message + } + + val bubbleDrawable = DisplayUtils.getMessageSelector( + bg_bubble_color, + resources.getColor(R.color.transparent), + bg_bubble_color, bubbleResource + ) + ViewCompat.setBackground(bubble, bubbleDrawable) + + val messageParameters = message.messageParameters + + itemView.setSelected(false) + messageTimeView!!.setTextColor(context!!.resources.getColor(R.color.warm_grey_four)) + + val layoutParams = messageTimeView!!.layoutParams as FlexboxLayout.LayoutParams + layoutParams.isWrapBefore = false + + var messageString: Spannable = SpannableString(message.text) + + var textSize = context!!.resources.getDimension(R.dimen.chat_text_size) + + if (messageParameters != null && messageParameters.size > 0) { + for (key in messageParameters.keys) { + val individualHashMap = message.messageParameters[key] + if (individualHashMap != null) { + if (individualHashMap["type"] == "user" || individualHashMap["type"] == "guest" || individualHashMap["type"] == "call") { + if (individualHashMap["id"] == message.activeUser.userId) { + messageString = DisplayUtils.searchAndReplaceWithMentionSpan( + messageText!!.context, + messageString, + individualHashMap["id"]!!, + individualHashMap["name"]!!, + individualHashMap["type"]!!, + message.activeUser, + R.xml.chip_you + ) + } else { + messageString = DisplayUtils.searchAndReplaceWithMentionSpan( + messageText!!.context, + messageString, + individualHashMap["id"]!!, + individualHashMap["name"]!!, + individualHashMap["type"]!!, + message.activeUser, + R.xml.chip_others + ) + } + } else if (individualHashMap["type"] == "file") { + itemView.setOnClickListener({ v -> + val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(individualHashMap["link"])) + context!!.startActivity(browserIntent) + }) + } + } + } + } else if (TextMatchers.isMessageWithSingleEmoticonOnly(message.text)) { + textSize = (textSize * 2.5).toFloat() + layoutParams.isWrapBefore = true + itemView.setSelected(true) + messageAuthor!!.visibility = View.GONE + } + + messageText!!.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize) + messageTimeView!!.layoutParams = layoutParams + messageText!!.text = messageString + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicOutcomingTextMessageViewHolder.java b/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicOutcomingTextMessageViewHolder.java index 8a6f6efbc..b5cb8ed7a 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicOutcomingTextMessageViewHolder.java +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicOutcomingTextMessageViewHolder.java @@ -103,7 +103,7 @@ public class MagicOutcomingTextMessageViewHolder individualHashMap.get("id"), individualHashMap.get("name"), individualHashMap.get("type"), - userUtils.getUserById(message.getActiveUser().getUserId()), + message.activeUser, R.xml.chip_others); } else if (individualHashMap.get("type").equals("file")) { itemView.setOnClickListener(v -> { @@ -122,7 +122,7 @@ public class MagicOutcomingTextMessageViewHolder } Resources resources = NextcloudTalkApplication.Companion.getSharedApplication().getResources(); - if (message.isGrouped) { + if (message.grouped) { Drawable bubbleDrawable = DisplayUtils.INSTANCE.getMessageSelector( resources.getColor(R.color.bg_message_list_outcoming_bubble), diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.kt index 4fbe7adf8..2ed2dca96 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.kt @@ -43,12 +43,12 @@ import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedA import com.nextcloud.talk.components.filebrowser.models.BrowserFile import com.nextcloud.talk.components.filebrowser.models.DavResponse import com.nextcloud.talk.components.filebrowser.webdav.ReadFilesystemOperation -import com.nextcloud.talk.models.database.UserEntity import com.nextcloud.talk.models.json.chat.ChatMessage import com.nextcloud.talk.models.json.chat.ChatMessage.MessageType.SINGLE_LINK_GIPHY_MESSAGE import com.nextcloud.talk.models.json.chat.ChatMessage.MessageType.SINGLE_LINK_IMAGE_MESSAGE import com.nextcloud.talk.models.json.chat.ChatMessage.MessageType.SINGLE_LINK_TENOR_MESSAGE import com.nextcloud.talk.models.json.chat.ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE +import com.nextcloud.talk.newarch.local.models.UserNgEntity import com.nextcloud.talk.utils.AccountUtils.canWeOpenFilesApp import com.nextcloud.talk.utils.DisplayUtils.setClickableString import com.nextcloud.talk.utils.DrawableUtils.getDrawableResourceIdForMimeType @@ -80,8 +80,8 @@ class MagicPreviewMessageViewHolder(itemView: View?) : IncomingImageMessageViewH override fun onBind(message: ChatMessage) { super.onBind(message) if (userAvatar != null) { - if (message.isGrouped || message.isOneToOneConversation) { - if (message.isOneToOneConversation) { + if (message.grouped || message.oneToOneConversation) { + if (message.oneToOneConversation) { userAvatar.visibility = View.GONE } else { userAvatar.visibility = View.INVISIBLE @@ -183,7 +183,7 @@ class MagicPreviewMessageViewHolder(itemView: View?) : IncomingImageMessageViewH private fun fetchFileInformation( url: String, - activeUser: UserEntity? + activeUser: UserNgEntity? ) { Single.fromCallable { ReadFilesystemOperation( 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 d7d4b7dd7..b59b28d09 100644 --- a/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt +++ b/app/src/main/java/com/nextcloud/talk/application/NextcloudTalkApplication.kt @@ -246,15 +246,25 @@ class NextcloudTalkApplication : Application(), LifecycleObserver { var userNg: UserNgEntity val newUsers = mutableListOf() for (user in users) { - userNg = UserNgEntity() - userNg.userId = user.userId - userNg.username = user.username + userNg = UserNgEntity(user.id, user.userId, user.username, user.baseUrl) userNg.token = user.token userNg.displayName = user.displayName - userNg.pushConfiguration = LoganSquare.parse(user.pushConfigurationState, PushConfigurationState::class.java) - userNg.capabilities = LoganSquare.parse(user.capabilities, Capabilities::class.java) + try { + userNg.pushConfiguration = + LoganSquare.parse(user.pushConfigurationState, PushConfigurationState::class.java) + } catch (e: Exception) { + // no push + } + if (user.capabilities != null) { + userNg.capabilities = LoganSquare.parse(user.capabilities, Capabilities::class.java) + } userNg.clientCertificate = user.clientCertificate - userNg.externalSignaling = LoganSquare.parse(user.externalSignalingServer, ExternalSignalingServer::class.java) + try { + userNg.externalSignaling = + LoganSquare.parse(user.externalSignalingServer, ExternalSignalingServer::class.java) + } catch (e: Exception) { + // no external signaling + } if (user.current) { userNg.status = ACTIVE } else { diff --git a/app/src/main/java/com/nextcloud/talk/callbacks/MentionAutocompleteCallback.java b/app/src/main/java/com/nextcloud/talk/callbacks/MentionAutocompleteCallback.java index e58780364..ba362ade9 100644 --- a/app/src/main/java/com/nextcloud/talk/callbacks/MentionAutocompleteCallback.java +++ b/app/src/main/java/com/nextcloud/talk/callbacks/MentionAutocompleteCallback.java @@ -28,6 +28,7 @@ import com.facebook.widget.text.span.BetterImageSpan; import com.nextcloud.talk.R; import com.nextcloud.talk.models.database.UserEntity; import com.nextcloud.talk.models.json.mention.Mention; +import com.nextcloud.talk.newarch.local.models.UserNgEntity; import com.nextcloud.talk.utils.DisplayUtils; import com.nextcloud.talk.utils.MagicCharPolicy; import com.nextcloud.talk.utils.text.Spans; @@ -37,10 +38,10 @@ import com.vanniktech.emoji.EmojiUtils; public class MentionAutocompleteCallback implements AutocompleteCallback { private Context context; - private UserEntity conversationUser; + private UserNgEntity conversationUser; private EditText editText; - public MentionAutocompleteCallback(Context context, UserEntity conversationUser, + public MentionAutocompleteCallback(Context context, UserNgEntity conversationUser, EditText editText) { this.context = context; this.conversationUser = conversationUser; diff --git a/app/src/main/java/com/nextcloud/talk/components/filebrowser/adapters/items/BrowserFileItem.kt b/app/src/main/java/com/nextcloud/talk/components/filebrowser/adapters/items/BrowserFileItem.kt index b22c04974..2062fbc7a 100644 --- a/app/src/main/java/com/nextcloud/talk/components/filebrowser/adapters/items/BrowserFileItem.kt +++ b/app/src/main/java/com/nextcloud/talk/components/filebrowser/adapters/items/BrowserFileItem.kt @@ -35,6 +35,8 @@ import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.components.filebrowser.models.BrowserFile import com.nextcloud.talk.interfaces.SelectionInterface import com.nextcloud.talk.models.database.UserEntity +import com.nextcloud.talk.newarch.local.models.UserNgEntity +import com.nextcloud.talk.newarch.local.models.getCredentials import com.nextcloud.talk.newarch.utils.getCredentials import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.DateUtils @@ -49,7 +51,7 @@ import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) class BrowserFileItem( val model: BrowserFile, - private val activeUser: UserEntity, + private val activeUser: UserNgEntity, private val selectionInterface: SelectionInterface ) : AbstractFlexibleItem(), IFilterable { @JvmField @@ -63,9 +65,9 @@ class BrowserFileItem( .inject(this) } - override fun equals(o: Any?): Boolean { - if (o is BrowserFileItem) { - val inItem = o as BrowserFileItem? + override fun equals(other: Any?): Boolean { + if (other is BrowserFileItem) { + val inItem = other as BrowserFileItem? return model.path == inItem!!.model.path } 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 ac37945b3..949d4893d 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 @@ -50,6 +50,7 @@ import com.nextcloud.talk.controllers.base.BaseController; import com.nextcloud.talk.interfaces.SelectionInterface; import com.nextcloud.talk.jobs.ShareOperationWorker; import com.nextcloud.talk.models.database.UserEntity; +import com.nextcloud.talk.newarch.local.models.UserNgEntity; import com.nextcloud.talk.utils.bundle.BundleKeys; import com.nextcloud.talk.utils.database.user.UserUtils; import eu.davidea.fastscroller.FastScroller; @@ -73,8 +74,6 @@ import org.parceler.Parcels; public class BrowserController extends BaseController implements ListingInterface, FlexibleAdapter.OnItemClickListener, SelectionInterface { private final Set selectedPaths; - @Inject - UserUtils userUtils; @BindView(R.id.recyclerView) RecyclerView recyclerView; @BindView(R.id.fast_scroller) @@ -97,7 +96,7 @@ public class BrowserController extends BaseController implements ListingInterfac private ListingAbstractClass listingAbstractClass; private BrowserType browserType; private String currentPath; - private UserEntity activeUser; + private UserNgEntity activeUser; private String roomToken; public BrowserController(Bundle args) { diff --git a/app/src/main/java/com/nextcloud/talk/components/filebrowser/operations/DavListing.java b/app/src/main/java/com/nextcloud/talk/components/filebrowser/operations/DavListing.java index 64d82941d..42c057c81 100644 --- a/app/src/main/java/com/nextcloud/talk/components/filebrowser/operations/DavListing.java +++ b/app/src/main/java/com/nextcloud/talk/components/filebrowser/operations/DavListing.java @@ -25,6 +25,7 @@ import com.nextcloud.talk.components.filebrowser.interfaces.ListingInterface; import com.nextcloud.talk.components.filebrowser.models.DavResponse; import com.nextcloud.talk.components.filebrowser.webdav.ReadFilesystemOperation; import com.nextcloud.talk.models.database.UserEntity; +import com.nextcloud.talk.newarch.local.models.UserNgEntity; import io.reactivex.Single; import io.reactivex.SingleObserver; import io.reactivex.disposables.Disposable; @@ -40,7 +41,7 @@ public class DavListing extends ListingAbstractClass { } @Override - public void getFiles(String path, UserEntity currentUser, @Nullable OkHttpClient okHttpClient) { + public void getFiles(String path, UserNgEntity currentUser, @Nullable OkHttpClient okHttpClient) { Single.fromCallable(new Callable() { @Override public ReadFilesystemOperation call() { diff --git a/app/src/main/java/com/nextcloud/talk/components/filebrowser/operations/ListingAbstractClass.java b/app/src/main/java/com/nextcloud/talk/components/filebrowser/operations/ListingAbstractClass.java index 08f30e01a..899a2aca7 100644 --- a/app/src/main/java/com/nextcloud/talk/components/filebrowser/operations/ListingAbstractClass.java +++ b/app/src/main/java/com/nextcloud/talk/components/filebrowser/operations/ListingAbstractClass.java @@ -24,6 +24,7 @@ import android.os.Handler; import androidx.annotation.Nullable; import com.nextcloud.talk.components.filebrowser.interfaces.ListingInterface; import com.nextcloud.talk.models.database.UserEntity; +import com.nextcloud.talk.newarch.local.models.UserNgEntity; import okhttp3.OkHttpClient; public abstract class ListingAbstractClass { @@ -35,7 +36,7 @@ public abstract class ListingAbstractClass { this.listingInterface = listingInterface; } - public abstract void getFiles(String path, UserEntity currentUser, + public abstract void getFiles(String path, UserNgEntity currentUser, @Nullable OkHttpClient okHttpClient); public void cancelAllJobs() { diff --git a/app/src/main/java/com/nextcloud/talk/components/filebrowser/webdav/ReadFilesystemOperation.java b/app/src/main/java/com/nextcloud/talk/components/filebrowser/webdav/ReadFilesystemOperation.java index 96f3de00f..e462999a2 100644 --- a/app/src/main/java/com/nextcloud/talk/components/filebrowser/webdav/ReadFilesystemOperation.java +++ b/app/src/main/java/com/nextcloud/talk/components/filebrowser/webdav/ReadFilesystemOperation.java @@ -27,6 +27,7 @@ import com.nextcloud.talk.components.filebrowser.models.BrowserFile; import com.nextcloud.talk.components.filebrowser.models.DavResponse; import com.nextcloud.talk.dagger.modules.RestModule; import com.nextcloud.talk.models.database.UserEntity; +import com.nextcloud.talk.newarch.local.models.UserNgEntity; import com.nextcloud.talk.utils.ApiUtils; import java.io.IOException; import java.util.ArrayList; @@ -42,7 +43,7 @@ public class ReadFilesystemOperation { private final int depth; private final String basePath; - public ReadFilesystemOperation(OkHttpClient okHttpClient, UserEntity currentUser, String path, + public ReadFilesystemOperation(OkHttpClient okHttpClient, UserNgEntity currentUser, String path, int depth) { OkHttpClient.Builder okHttpClientBuilder = okHttpClient.newBuilder(); okHttpClientBuilder.followRedirects(false); diff --git a/app/src/main/java/com/nextcloud/talk/controllers/CallController.java b/app/src/main/java/com/nextcloud/talk/controllers/CallController.java deleted file mode 100644 index 3dbaa925f..000000000 --- a/app/src/main/java/com/nextcloud/talk/controllers/CallController.java +++ /dev/null @@ -1,2400 +0,0 @@ -/* - * Nextcloud Talk application - * - * @author Mario Danic - * Copyright (C) 2017-2018 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.controllers; - -import android.Manifest; -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.annotation.SuppressLint; -import android.content.res.Configuration; -import android.graphics.Color; -import android.media.AudioAttributes; -import android.media.MediaPlayer; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.text.TextUtils; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.widget.FrameLayout; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.ProgressBar; -import android.widget.RelativeLayout; -import android.widget.TextView; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatActivity; -import autodagger.AutoInjector; -import butterknife.BindView; -import butterknife.OnClick; -import butterknife.OnLongClick; -import com.bluelinelabs.logansquare.LoganSquare; -import com.facebook.drawee.backends.pipeline.Fresco; -import com.facebook.drawee.interfaces.DraweeController; -import com.facebook.drawee.view.SimpleDraweeView; -import com.nextcloud.talk.R; -import com.nextcloud.talk.api.NcApi; -import com.nextcloud.talk.application.NextcloudTalkApplication; -import com.nextcloud.talk.controllers.base.BaseController; -import com.nextcloud.talk.events.ConfigurationChangeEvent; -import com.nextcloud.talk.events.MediaStreamEvent; -import com.nextcloud.talk.events.NetworkEvent; -import com.nextcloud.talk.events.PeerConnectionEvent; -import com.nextcloud.talk.events.SessionDescriptionSendEvent; -import com.nextcloud.talk.events.WebSocketCommunicationEvent; -import com.nextcloud.talk.models.ExternalSignalingServer; -import com.nextcloud.talk.models.database.UserEntity; -import com.nextcloud.talk.models.json.capabilities.CapabilitiesOverall; -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.participants.Participant; -import com.nextcloud.talk.models.json.participants.ParticipantsOverall; -import com.nextcloud.talk.models.json.signaling.DataChannelMessage; -import com.nextcloud.talk.models.json.signaling.DataChannelMessageNick; -import com.nextcloud.talk.models.json.signaling.NCIceCandidate; -import com.nextcloud.talk.models.json.signaling.NCMessagePayload; -import com.nextcloud.talk.models.json.signaling.NCMessageWrapper; -import com.nextcloud.talk.models.json.signaling.NCSignalingMessage; -import com.nextcloud.talk.models.json.signaling.Signaling; -import com.nextcloud.talk.models.json.signaling.SignalingOverall; -import com.nextcloud.talk.models.json.signaling.settings.IceServer; -import com.nextcloud.talk.models.json.signaling.settings.SignalingSettingsOverall; -import com.nextcloud.talk.utils.ApiUtils; -import com.nextcloud.talk.utils.DisplayUtils; -import com.nextcloud.talk.utils.NotificationUtils; -import com.nextcloud.talk.utils.animations.PulseAnimation; -import com.nextcloud.talk.utils.bundle.BundleKeys; -import com.nextcloud.talk.utils.database.user.UserUtils; -import com.nextcloud.talk.utils.power.PowerManagerUtils; -import com.nextcloud.talk.utils.preferences.AppPreferences; -import com.nextcloud.talk.utils.singletons.ApplicationWideCurrentRoomHolder; -import com.nextcloud.talk.webrtc.MagicAudioManager; -import com.nextcloud.talk.webrtc.MagicPeerConnectionWrapper; -import com.nextcloud.talk.webrtc.MagicWebRTCUtils; -import com.nextcloud.talk.webrtc.MagicWebSocketInstance; -import com.nextcloud.talk.webrtc.WebSocketConnectionHelper; -import com.wooplr.spotlight.SpotlightView; -import io.reactivex.Observable; -import io.reactivex.Observer; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; -import java.io.IOException; -import java.net.CookieManager; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.concurrent.TimeUnit; -import javax.inject.Inject; -import me.zhanghai.android.effortlesspermissions.AfterPermissionDenied; -import me.zhanghai.android.effortlesspermissions.EffortlessPermissions; -import me.zhanghai.android.effortlesspermissions.OpenAppDetailsDialogFragment; -import org.apache.commons.lang3.StringEscapeUtils; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; -import org.parceler.Parcel; -import org.webrtc.AudioSource; -import org.webrtc.AudioTrack; -import org.webrtc.Camera1Enumerator; -import org.webrtc.Camera2Enumerator; -import org.webrtc.CameraEnumerator; -import org.webrtc.CameraVideoCapturer; -import org.webrtc.EglBase; -import org.webrtc.IceCandidate; -import org.webrtc.Logging; -import org.webrtc.MediaConstraints; -import org.webrtc.MediaStream; -import org.webrtc.PeerConnection; -import org.webrtc.PeerConnectionFactory; -import org.webrtc.RendererCommon; -import org.webrtc.SessionDescription; -import org.webrtc.SurfaceViewRenderer; -import org.webrtc.VideoCapturer; -import org.webrtc.VideoSource; -import org.webrtc.VideoTrack; -import pub.devrel.easypermissions.AfterPermissionGranted; - -@AutoInjector(NextcloudTalkApplication.class) -public class CallController extends BaseController { - - private static final String TAG = "CallController"; - - private static final String[] PERMISSIONS_CALL = { - android.Manifest.permission.CAMERA, - android.Manifest.permission.RECORD_AUDIO, - }; - - private static final String[] PERMISSIONS_CAMERA = { - Manifest.permission.CAMERA - }; - - private static final String[] PERMISSIONS_MICROPHONE = { - Manifest.permission.RECORD_AUDIO - }; - - @BindView(R.id.callControlEnableSpeaker) - SimpleDraweeView callControlEnableSpeaker; - - @BindView(R.id.pip_video_view) - SurfaceViewRenderer pipVideoView; - @BindView(R.id.relative_layout) - RelativeLayout relativeLayout; - @BindView(R.id.remote_renderers_layout) - LinearLayout remoteRenderersLayout; - - @BindView(R.id.callControlsRelativeLayout) - RelativeLayout callControls; - @BindView(R.id.call_control_microphone) - SimpleDraweeView microphoneControlButton; - @BindView(R.id.call_control_camera) - SimpleDraweeView cameraControlButton; - @BindView(R.id.call_control_switch_camera) - SimpleDraweeView cameraSwitchButton; - @BindView(R.id.connectingTextView) - TextView connectingTextView; - - @BindView(R.id.connectingRelativeLayoutView) - RelativeLayout connectingView; - - @BindView(R.id.conversationRelativeLayoutView) - RelativeLayout conversationView; - - @BindView(R.id.errorImageView) - ImageView errorImageView; - - @BindView(R.id.progress_bar) - ProgressBar progressBar; - - @Inject - NcApi ncApi; - @Inject - UserUtils userUtils; - @Inject - AppPreferences appPreferences; - @Inject - CookieManager cookieManager; - @Inject - EventBus eventBus; - - private PeerConnectionFactory peerConnectionFactory; - private MediaConstraints audioConstraints; - private MediaConstraints videoConstraints; - private MediaConstraints sdpConstraints; - private MediaConstraints sdpConstraintsForMCU; - private MagicAudioManager audioManager; - private VideoSource videoSource; - private VideoTrack localVideoTrack; - private AudioSource audioSource; - private AudioTrack localAudioTrack; - private VideoCapturer videoCapturer; - private EglBase rootEglBase; - private Disposable signalingDisposable; - private Disposable pingDisposable; - private List iceServers; - private CameraEnumerator cameraEnumerator; - private String roomToken; - private UserEntity conversationUser; - private String callSession; - private MediaStream localMediaStream; - private String credentials; - private List magicPeerConnectionWrapperList = new ArrayList<>(); - private Map participantMap = new HashMap<>(); - - private boolean videoOn = false; - private boolean audioOn = false; - - private boolean isMultiSession = false; - private boolean needsPing = true; - - private boolean isVoiceOnlyCall; - private Handler callControlHandler = new Handler(); - private Handler cameraSwitchHandler = new Handler(); - - private boolean isPTTActive = false; - private PulseAnimation pulseAnimation; - private View.OnClickListener videoOnClickListener; - - private String baseUrl; - private String roomId; - - private SpotlightView spotlightView; - - private ExternalSignalingServer externalSignalingServer; - private MagicWebSocketInstance webSocketClient; - private WebSocketConnectionHelper webSocketConnectionHelper; - private boolean hasMCU; - private boolean hasExternalSignalingServer; - private String conversationPassword; - - private PowerManagerUtils powerManagerUtils; - - private Handler handler; - - private CallStatus currentCallStatus; - - private MediaPlayer mediaPlayer; - - public CallController(Bundle args) { - super(); - NextcloudTalkApplication.Companion.getSharedApplication() - .getComponentApplication() - .inject(this); - - roomId = args.getString(BundleKeys.INSTANCE.getKEY_ROOM_ID(), ""); - roomToken = args.getString(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN(), ""); - conversationUser = args.getParcelable(BundleKeys.INSTANCE.getKEY_USER_ENTITY()); - conversationPassword = args.getString(BundleKeys.INSTANCE.getKEY_CONVERSATION_PASSWORD(), ""); - isVoiceOnlyCall = args.getBoolean(BundleKeys.INSTANCE.getKEY_CALL_VOICE_ONLY(), false); - - credentials = - ApiUtils.getCredentials(conversationUser.getUsername(), conversationUser.getToken()); - - baseUrl = args.getString(BundleKeys.INSTANCE.getKEY_MODIFIED_BASE_URL(), ""); - - if (TextUtils.isEmpty(baseUrl)) { - baseUrl = conversationUser.getBaseUrl(); - } - - powerManagerUtils = new PowerManagerUtils(); - setCallState(CallStatus.CALLING); - } - - @Override - protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) { - return inflater.inflate(R.layout.controller_call, container, false); - } - - private void createCameraEnumerator() { - if (getActivity() != null) { - boolean camera2EnumeratorIsSupported = false; - try { - camera2EnumeratorIsSupported = Camera2Enumerator.isSupported(getActivity()); - } catch (final Throwable throwable) { - Log.w(TAG, "Camera2Enumator threw an error"); - } - - if (camera2EnumeratorIsSupported) { - cameraEnumerator = new Camera2Enumerator(getActivity()); - } else { - cameraEnumerator = - new Camera1Enumerator(MagicWebRTCUtils.shouldEnableVideoHardwareAcceleration()); - } - } - } - - @Override - protected void onViewBound(@NonNull View view) { - super.onViewBound(view); - - microphoneControlButton.setOnTouchListener(new MicrophoneButtonTouchListener()); - videoOnClickListener = new VideoClickListener(); - - pulseAnimation = PulseAnimation.create().with(microphoneControlButton) - .setDuration(310) - .setRepeatCount(PulseAnimation.INFINITE) - .setRepeatMode(PulseAnimation.REVERSE); - - setPipVideoViewDimensions(); - - callControls.setZ(100.0f); - basicInitialization(); - initViews(); - - initiateCall(); - } - - private void basicInitialization() { - rootEglBase = EglBase.create(); - createCameraEnumerator(); - - //Create a new PeerConnectionFactory instance. - PeerConnectionFactory.Options options = new PeerConnectionFactory.Options(); - peerConnectionFactory = PeerConnectionFactory.builder().createPeerConnectionFactory(); - - peerConnectionFactory.setVideoHwAccelerationOptions(rootEglBase.getEglBaseContext(), - rootEglBase.getEglBaseContext()); - - //Create MediaConstraints - Will be useful for specifying video and audio constraints. - audioConstraints = new MediaConstraints(); - videoConstraints = new MediaConstraints(); - - localMediaStream = peerConnectionFactory.createLocalMediaStream("NCMS"); - - // Create and audio manager that will take care of audio routing, - // audio modes, audio device enumeration etc. - audioManager = MagicAudioManager.create(getApplicationContext(), !isVoiceOnlyCall); - // Store existing audio settings and change audio mode to - // MODE_IN_COMMUNICATION for best possible VoIP performance. - Log.d(TAG, "Starting the audio manager..."); - audioManager.start(this::onAudioManagerDevicesChanged); - - iceServers = new ArrayList<>(); - - //create sdpConstraints - sdpConstraints = new MediaConstraints(); - sdpConstraintsForMCU = new MediaConstraints(); - sdpConstraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true")); - String offerToReceiveVideoString = "true"; - - if (isVoiceOnlyCall) { - offerToReceiveVideoString = "false"; - } - - sdpConstraints.mandatory.add( - new MediaConstraints.KeyValuePair("OfferToReceiveVideo", offerToReceiveVideoString)); - - sdpConstraintsForMCU.mandatory.add( - new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "false")); - sdpConstraintsForMCU.mandatory.add( - new MediaConstraints.KeyValuePair("OfferToReceiveVideo", "false")); - - sdpConstraintsForMCU.optional.add( - new MediaConstraints.KeyValuePair("internalSctpDataChannels", "true")); - sdpConstraintsForMCU.optional.add( - new MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true")); - - sdpConstraints.optional.add( - new MediaConstraints.KeyValuePair("internalSctpDataChannels", "true")); - sdpConstraints.optional.add(new MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true")); - - if (!isVoiceOnlyCall) { - cameraInitialization(); - } - - microphoneInitialization(); - } - - private void handleFromNotification() { - ncApi.getRooms(credentials, ApiUtils.getUrlForGetRooms(baseUrl)) - .retry(3) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - - } - - @Override - public void onNext(RoomsOverall roomsOverall) { - for (Conversation conversation : roomsOverall.getOcs().getData()) { - if (roomId.equals(conversation.getConversationId())) { - roomToken = conversation.getToken(); - break; - } - } - - checkPermissions(); - } - - @Override - public void onError(Throwable e) { - - } - - @Override - public void onComplete() { - - } - }); - } - - private void initViews() { - if (isVoiceOnlyCall) { - callControlEnableSpeaker.setVisibility(View.VISIBLE); - cameraSwitchButton.setVisibility(View.GONE); - cameraControlButton.setVisibility(View.GONE); - pipVideoView.setVisibility(View.GONE); - } else { - if (cameraEnumerator.getDeviceNames().length < 2) { - cameraSwitchButton.setVisibility(View.GONE); - } - - pipVideoView.init(rootEglBase.getEglBaseContext(), null); - pipVideoView.setZOrderMediaOverlay(true); - // disabled because it causes some devices to crash - pipVideoView.setEnableHardwareScaler(false); - pipVideoView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT); - } - } - - private void checkPermissions() { - if (isVoiceOnlyCall) { - onMicrophoneClick(); - } else if (getActivity() != null) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - requestPermissions(PERMISSIONS_CALL, 100); - } else { - onRequestPermissionsResult(100, PERMISSIONS_CALL, new int[] { 1, 1 }); - } - } - } - - private boolean isConnectionEstablished() { - return (currentCallStatus.equals(CallStatus.ESTABLISHED) || currentCallStatus.equals( - CallStatus.IN_CONVERSATION)); - } - - @AfterPermissionGranted(100) - private void onPermissionsGranted() { - if (EffortlessPermissions.hasPermissions(getActivity(), PERMISSIONS_CALL)) { - if (!videoOn && !isVoiceOnlyCall) { - onCameraClick(); - } - - if (!audioOn) { - onMicrophoneClick(); - } - - if (!isVoiceOnlyCall) { - if (cameraEnumerator.getDeviceNames().length == 0) { - cameraControlButton.setVisibility(View.GONE); - } - - if (cameraEnumerator.getDeviceNames().length > 1) { - cameraSwitchButton.setVisibility(View.VISIBLE); - } - } - - if (!isConnectionEstablished()) { - fetchSignalingSettings(); - } - } else if (getActivity() != null && EffortlessPermissions.somePermissionPermanentlyDenied( - getActivity(), - PERMISSIONS_CALL)) { - checkIfSomeAreApproved(); - } - } - - private void checkIfSomeAreApproved() { - if (!isVoiceOnlyCall) { - if (cameraEnumerator.getDeviceNames().length == 0) { - cameraControlButton.setVisibility(View.GONE); - } - - if (cameraEnumerator.getDeviceNames().length > 1) { - cameraSwitchButton.setVisibility(View.VISIBLE); - } - - if (getActivity() != null && EffortlessPermissions.hasPermissions(getActivity(), - PERMISSIONS_CAMERA)) { - if (!videoOn) { - onCameraClick(); - } - } else { - cameraControlButton.getHierarchy() - .setPlaceholderImage(R.drawable.ic_videocam_off_white_24px); - cameraControlButton.setAlpha(0.7f); - cameraSwitchButton.setVisibility(View.GONE); - } - } - - if (EffortlessPermissions.hasPermissions(getActivity(), PERMISSIONS_MICROPHONE)) { - if (!audioOn) { - onMicrophoneClick(); - } - } else { - microphoneControlButton.getHierarchy().setPlaceholderImage(R.drawable.ic_mic_off_white_24px); - } - - if (!isConnectionEstablished()) { - fetchSignalingSettings(); - } - } - - @AfterPermissionDenied(100) - private void onPermissionsDenied() { - if (!isVoiceOnlyCall) { - if (cameraEnumerator.getDeviceNames().length == 0) { - cameraControlButton.setVisibility(View.GONE); - } else if (cameraEnumerator.getDeviceNames().length == 1) { - cameraSwitchButton.setVisibility(View.GONE); - } - } - - if (getActivity() != null && (EffortlessPermissions.hasPermissions(getActivity(), - PERMISSIONS_CAMERA) || - EffortlessPermissions.hasPermissions(getActivity(), PERMISSIONS_MICROPHONE))) { - checkIfSomeAreApproved(); - } else if (!isConnectionEstablished()) { - fetchSignalingSettings(); - } - } - - private void onAudioManagerDevicesChanged( - final MagicAudioManager.AudioDevice device, - final Set availableDevices) { - Log.d(TAG, "onAudioManagerDevicesChanged: " + availableDevices + ", " - + "selected: " + device); - - final boolean shouldDisableProximityLock = - (device.equals(MagicAudioManager.AudioDevice.WIRED_HEADSET) - || device.equals(MagicAudioManager.AudioDevice.SPEAKER_PHONE) - || device.equals(MagicAudioManager.AudioDevice.BLUETOOTH)); - - if (shouldDisableProximityLock) { - powerManagerUtils.updatePhoneState( - PowerManagerUtils.PhoneState.WITHOUT_PROXIMITY_SENSOR_LOCK); - } else { - powerManagerUtils.updatePhoneState(PowerManagerUtils.PhoneState.WITH_PROXIMITY_SENSOR_LOCK); - } - } - - private void cameraInitialization() { - videoCapturer = createCameraCapturer(cameraEnumerator); - - //Create a VideoSource instance - if (videoCapturer != null) { - videoSource = peerConnectionFactory.createVideoSource(videoCapturer); - localVideoTrack = peerConnectionFactory.createVideoTrack("NCv0", videoSource); - localMediaStream.addTrack(localVideoTrack); - localVideoTrack.setEnabled(false); - localVideoTrack.addSink(pipVideoView); - } - } - - private void microphoneInitialization() { - //create an AudioSource instance - audioSource = peerConnectionFactory.createAudioSource(audioConstraints); - localAudioTrack = peerConnectionFactory.createAudioTrack("NCa0", audioSource); - localAudioTrack.setEnabled(false); - localMediaStream.addTrack(localAudioTrack); - } - - private VideoCapturer createCameraCapturer(CameraEnumerator enumerator) { - final String[] deviceNames = enumerator.getDeviceNames(); - - // First, try to find front facing camera - Logging.d(TAG, "Looking for front facing cameras."); - for (String deviceName : deviceNames) { - if (enumerator.isFrontFacing(deviceName)) { - Logging.d(TAG, "Creating front facing camera capturer."); - VideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null); - if (videoCapturer != null) { - pipVideoView.setMirror(true); - return videoCapturer; - } - } - } - - // Front facing camera not found, try something else - Logging.d(TAG, "Looking for other cameras."); - for (String deviceName : deviceNames) { - if (!enumerator.isFrontFacing(deviceName)) { - Logging.d(TAG, "Creating other camera capturer."); - VideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null); - - if (videoCapturer != null) { - pipVideoView.setMirror(false); - return videoCapturer; - } - } - } - - return null; - } - - @OnLongClick(R.id.call_control_microphone) - boolean onMicrophoneLongClick() { - if (!audioOn) { - callControlHandler.removeCallbacksAndMessages(null); - cameraSwitchHandler.removeCallbacksAndMessages(null); - isPTTActive = true; - callControls.setVisibility(View.VISIBLE); - if (!isVoiceOnlyCall) { - cameraSwitchButton.setVisibility(View.VISIBLE); - } - } - - onMicrophoneClick(); - return true; - } - - @OnClick(R.id.callControlEnableSpeaker) - public void onEnableSpeakerphoneClick() { - if (audioManager != null) { - audioManager.toggleUseSpeakerphone(); - if (audioManager.isSpeakerphoneAutoOn()) { - callControlEnableSpeaker.getHierarchy() - .setPlaceholderImage(R.drawable.ic_volume_up_white_24dp); - } else { - callControlEnableSpeaker.getHierarchy() - .setPlaceholderImage(R.drawable.ic_volume_mute_white_24dp); - } - } - } - - @OnClick(R.id.call_control_microphone) - public void onMicrophoneClick() { - if (getActivity() != null && EffortlessPermissions.hasPermissions(getActivity(), - PERMISSIONS_MICROPHONE)) { - - if (getActivity() != null && !appPreferences.getPushToTalkIntroShown()) { - spotlightView = new SpotlightView.Builder(getActivity()) - .introAnimationDuration(300) - .enableRevealAnimation(true) - .performClick(false) - .fadeinTextDuration(400) - .headingTvColor(getResources().getColor(R.color.colorPrimary)) - .headingTvSize(20) - .headingTvText(getResources().getString(R.string.nc_push_to_talk)) - .subHeadingTvColor(getResources().getColor(R.color.bg_default)) - .subHeadingTvSize(16) - .subHeadingTvText(getResources().getString(R.string.nc_push_to_talk_desc)) - .maskColor(Color.parseColor("#dc000000")) - .target(microphoneControlButton) - .lineAnimDuration(400) - .lineAndArcColor(getResources().getColor(R.color.colorPrimary)) - .enableDismissAfterShown(true) - .dismissOnBackPress(true) - .usageId("pushToTalk") - .show(); - - appPreferences.setPushToTalkIntroShown(true); - } - - if (!isPTTActive) { - audioOn = !audioOn; - - if (audioOn) { - microphoneControlButton.getHierarchy().setPlaceholderImage(R.drawable.ic_mic_white_24px); - } else { - microphoneControlButton.getHierarchy() - .setPlaceholderImage(R.drawable.ic_mic_off_white_24px); - } - - toggleMedia(audioOn, false); - } else { - microphoneControlButton.getHierarchy().setPlaceholderImage(R.drawable.ic_mic_white_24px); - pulseAnimation.start(); - toggleMedia(true, false); - } - - if (isVoiceOnlyCall && !isConnectionEstablished()) { - fetchSignalingSettings(); - } - } else if (getActivity() != null && EffortlessPermissions.somePermissionPermanentlyDenied( - getActivity(), - PERMISSIONS_MICROPHONE)) { - // Microphone permission is permanently denied so we cannot request it normally. - - OpenAppDetailsDialogFragment.show( - R.string.nc_microphone_permission_permanently_denied, - R.string.nc_permissions_settings, (AppCompatActivity) getActivity()); - } else { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - requestPermissions(PERMISSIONS_MICROPHONE, 100); - } else { - onRequestPermissionsResult(100, PERMISSIONS_MICROPHONE, new int[] { 1 }); - } - } - } - - @OnClick(R.id.callControlHangupView) - void onHangupClick() { - setCallState(CallStatus.LEAVING); - hangup(true); - } - - @OnClick(R.id.call_control_camera) - public void onCameraClick() { - if (getActivity() != null && EffortlessPermissions.hasPermissions(getActivity(), - PERMISSIONS_CAMERA)) { - videoOn = !videoOn; - - if (videoOn) { - cameraControlButton.getHierarchy().setPlaceholderImage(R.drawable.ic_videocam_white_24px); - if (cameraEnumerator.getDeviceNames().length > 1) { - cameraSwitchButton.setVisibility(View.VISIBLE); - } - } else { - cameraControlButton.getHierarchy() - .setPlaceholderImage(R.drawable.ic_videocam_off_white_24px); - cameraSwitchButton.setVisibility(View.GONE); - } - - toggleMedia(videoOn, true); - } else if (getActivity() != null && EffortlessPermissions.somePermissionPermanentlyDenied( - getActivity(), - PERMISSIONS_CAMERA)) { - // Camera permission is permanently denied so we cannot request it normally. - OpenAppDetailsDialogFragment.show( - R.string.nc_camera_permission_permanently_denied, - R.string.nc_permissions_settings, (AppCompatActivity) getActivity()); - } else { - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - requestPermissions(PERMISSIONS_CAMERA, 100); - } else { - onRequestPermissionsResult(100, PERMISSIONS_CAMERA, new int[] { 1 }); - } - } - } - - @OnClick({ R.id.call_control_switch_camera, R.id.pip_video_view }) - public void switchCamera() { - CameraVideoCapturer cameraVideoCapturer = (CameraVideoCapturer) videoCapturer; - if (cameraVideoCapturer != null) { - cameraVideoCapturer.switchCamera(new CameraVideoCapturer.CameraSwitchHandler() { - @Override - public void onCameraSwitchDone(boolean currentCameraIsFront) { - pipVideoView.setMirror(currentCameraIsFront); - } - - @Override - public void onCameraSwitchError(String s) { - - } - }); - } - } - - private void toggleMedia(boolean enable, boolean video) { - String message; - if (video) { - message = "videoOff"; - if (enable) { - cameraControlButton.setAlpha(1.0f); - message = "videoOn"; - startVideoCapture(); - } else { - cameraControlButton.setAlpha(0.7f); - if (videoCapturer != null) { - try { - videoCapturer.stopCapture(); - } catch (InterruptedException e) { - Log.d(TAG, "Failed to stop capturing video while sensor is near the ear"); - } - } - } - - if (localMediaStream != null && localMediaStream.videoTracks.size() > 0) { - localMediaStream.videoTracks.get(0).setEnabled(enable); - } - if (enable) { - pipVideoView.setVisibility(View.VISIBLE); - } else { - pipVideoView.setVisibility(View.INVISIBLE); - } - } else { - message = "audioOff"; - if (enable) { - message = "audioOn"; - microphoneControlButton.setAlpha(1.0f); - } else { - microphoneControlButton.setAlpha(0.7f); - } - - if (localMediaStream != null && localMediaStream.audioTracks.size() > 0) { - localMediaStream.audioTracks.get(0).setEnabled(enable); - } - } - - if (isConnectionEstablished()) { - if (!hasMCU) { - for (int i = 0; i < magicPeerConnectionWrapperList.size(); i++) { - magicPeerConnectionWrapperList.get(i).sendChannelData(new DataChannelMessage(message)); - } - } else { - for (int i = 0; i < magicPeerConnectionWrapperList.size(); i++) { - if (magicPeerConnectionWrapperList.get(i) - .getSessionId() - .equals(webSocketClient.getSessionId())) { - magicPeerConnectionWrapperList.get(i).sendChannelData(new DataChannelMessage(message)); - break; - } - } - } - } - } - - private void animateCallControls(boolean show, long startDelay) { - if (isVoiceOnlyCall) { - if (spotlightView != null && spotlightView.getVisibility() != View.GONE) { - spotlightView.setVisibility(View.GONE); - } - } else if (!isPTTActive) { - float alpha; - long duration; - - if (show) { - callControlHandler.removeCallbacksAndMessages(null); - cameraSwitchHandler.removeCallbacksAndMessages(null); - alpha = 1.0f; - duration = 1000; - if (callControls.getVisibility() != View.VISIBLE) { - callControls.setAlpha(0.0f); - callControls.setVisibility(View.VISIBLE); - - cameraSwitchButton.setAlpha(0.0f); - cameraSwitchButton.setVisibility(View.VISIBLE); - } else { - callControlHandler.postDelayed(() -> animateCallControls(false, 0), 5000); - return; - } - } else { - alpha = 0.0f; - duration = 1000; - } - - if (callControls != null) { - callControls.setEnabled(false); - callControls.animate() - .translationY(0) - .alpha(alpha) - .setDuration(duration) - .setStartDelay(startDelay) - .setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - super.onAnimationEnd(animation); - if (callControls != null) { - if (!show) { - callControls.setVisibility(View.GONE); - if (spotlightView != null && spotlightView.getVisibility() != View.GONE) { - spotlightView.setVisibility(View.GONE); - } - } else { - callControlHandler.postDelayed(new Runnable() { - @Override - public void run() { - if (!isPTTActive) { - animateCallControls(false, 0); - } - } - }, 7500); - } - - callControls.setEnabled(true); - } - } - }); - } - - if (cameraSwitchButton != null) { - cameraSwitchButton.setEnabled(false); - cameraSwitchButton.animate() - .translationY(0) - .alpha(alpha) - .setDuration(duration) - .setStartDelay(startDelay) - .setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - super.onAnimationEnd(animation); - if (cameraSwitchButton != null) { - if (!show) { - cameraSwitchButton.setVisibility(View.GONE); - } - - cameraSwitchButton.setEnabled(true); - } - } - }); - } - } - } - - @Override - public void onDestroy() { - if (!currentCallStatus.equals(CallStatus.LEAVING)) { - onHangupClick(); - } - powerManagerUtils.updatePhoneState(PowerManagerUtils.PhoneState.IDLE); - super.onDestroy(); - } - - private void fetchSignalingSettings() { - ncApi.getSignalingSettings(credentials, ApiUtils.getUrlForSignalingSettings(baseUrl)) - .subscribeOn(Schedulers.io()) - .retry(3) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - - } - - @Override - public void onNext(SignalingSettingsOverall signalingSettingsOverall) { - IceServer iceServer; - if (signalingSettingsOverall != null && signalingSettingsOverall.getOcs() != null && - signalingSettingsOverall.getOcs().getSettings() != null) { - - externalSignalingServer = new ExternalSignalingServer(); - - if (!TextUtils.isEmpty( - signalingSettingsOverall.getOcs().getSettings().getExternalSignalingServer()) && - !TextUtils.isEmpty(signalingSettingsOverall.getOcs() - .getSettings() - .getExternalSignalingTicket())) { - externalSignalingServer = new ExternalSignalingServer(); - externalSignalingServer.setExternalSignalingServer( - signalingSettingsOverall.getOcs().getSettings().getExternalSignalingServer()); - externalSignalingServer.setExternalSignalingTicket( - signalingSettingsOverall.getOcs().getSettings().getExternalSignalingTicket()); - hasExternalSignalingServer = true; - } else { - hasExternalSignalingServer = false; - } - - if (!conversationUser.getUserId().equals("?")) { - try { - userUtils.createOrUpdateUser(null, null, null, null, null, null, null, - conversationUser.getId(), null, null, - LoganSquare.serialize(externalSignalingServer)) - .subscribeOn(Schedulers.io()) - .subscribe(); - } catch (IOException exception) { - Log.e(TAG, "Failed to serialize external signaling server"); - } - } - - if (signalingSettingsOverall.getOcs().getSettings().getStunServers() != null) { - for (int i = 0; - i < signalingSettingsOverall.getOcs().getSettings().getStunServers().size(); - i++) { - iceServer = - signalingSettingsOverall.getOcs().getSettings().getStunServers().get(i); - if (TextUtils.isEmpty(iceServer.getUsername()) || TextUtils.isEmpty(iceServer - .getCredential())) { - iceServers.add(new PeerConnection.IceServer(iceServer.getUrl())); - } else { - iceServers.add(new PeerConnection.IceServer(iceServer.getUrl(), - iceServer.getUsername(), iceServer.getCredential())); - } - } - } - - if (signalingSettingsOverall.getOcs().getSettings().getTurnServers() != null) { - for (int i = 0; - i < signalingSettingsOverall.getOcs().getSettings().getTurnServers().size(); - i++) { - iceServer = - signalingSettingsOverall.getOcs().getSettings().getTurnServers().get(i); - for (int j = 0; j < iceServer.getUrls().size(); j++) { - if (TextUtils.isEmpty(iceServer.getUsername()) || TextUtils.isEmpty(iceServer - .getCredential())) { - iceServers.add(new PeerConnection.IceServer(iceServer.getUrls().get(j))); - } else { - iceServers.add(new PeerConnection.IceServer(iceServer.getUrls().get(j), - iceServer.getUsername(), iceServer.getCredential())); - } - } - } - } - } - - checkCapabilities(); - } - - @Override - public void onError(Throwable e) { - } - - @Override - public void onComplete() { - - } - }); - } - - private void checkCapabilities() { - ncApi.getCapabilities(credentials, ApiUtils.getUrlForCapabilities(baseUrl)) - .retry(3) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - - } - - @Override - public void onNext(CapabilitiesOverall capabilitiesOverall) { - isMultiSession = capabilitiesOverall.getOcs().getData() - .getCapabilities() != null && capabilitiesOverall.getOcs().getData() - .getCapabilities().getSpreedCapability() != null && - capabilitiesOverall.getOcs().getData() - .getCapabilities().getSpreedCapability() - .getFeatures() != null && capabilitiesOverall.getOcs().getData() - .getCapabilities().getSpreedCapability() - .getFeatures().contains("multi-room-users"); - - needsPing = !(capabilitiesOverall.getOcs().getData() - .getCapabilities() != null && capabilitiesOverall.getOcs().getData() - .getCapabilities().getSpreedCapability() != null && - capabilitiesOverall.getOcs().getData() - .getCapabilities().getSpreedCapability() - .getFeatures() != null && capabilitiesOverall.getOcs().getData() - .getCapabilities().getSpreedCapability() - .getFeatures().contains("no-ping")); - - if (!hasExternalSignalingServer) { - joinRoomAndCall(); - } else { - setupAndInitiateWebSocketsConnection(); - } - } - - @Override - public void onError(Throwable e) { - isMultiSession = false; - } - - @Override - public void onComplete() { - - } - }); - } - - private void joinRoomAndCall() { - ncApi.joinRoom(credentials, ApiUtils.getUrlForSettingMyselfAsActiveParticipant(baseUrl, - roomToken), conversationPassword) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .retry(3) - .subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - - } - - @Override - public void onNext(RoomOverall roomOverall) { - callSession = roomOverall.getOcs().getData().getSessionId(); - ApplicationWideCurrentRoomHolder.getInstance().setSession(callSession); - ApplicationWideCurrentRoomHolder.getInstance().setCurrentRoomId(roomId); - ApplicationWideCurrentRoomHolder.getInstance().setCurrentRoomToken(roomToken); - ApplicationWideCurrentRoomHolder.getInstance().setUserInRoom(conversationUser); - callOrJoinRoomViaWebSocket(); - } - - @Override - public void onError(Throwable e) { - - } - - @Override - public void onComplete() { - - } - }); - } - - private void callOrJoinRoomViaWebSocket() { - if (!hasExternalSignalingServer) { - performCall(); - } else { - webSocketClient.joinRoomWithRoomTokenAndSession(roomToken, callSession); - } - } - - private void performCall() { - ncApi.joinCall(credentials, - ApiUtils.getUrlForCall(baseUrl, roomToken)) - .subscribeOn(Schedulers.io()) - .retry(3) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - - } - - @Override - public void onNext(GenericOverall genericOverall) { - if (!currentCallStatus.equals(CallStatus.LEAVING)) { - setCallState(CallStatus.ESTABLISHED); - - ApplicationWideCurrentRoomHolder.getInstance().setInCall(true); - - if (needsPing) { - ncApi.pingCall(credentials, ApiUtils.getUrlForCallPing(baseUrl, roomToken)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .repeatWhen(observable -> observable.delay(5000, TimeUnit.MILLISECONDS)) - .takeWhile(observable -> isConnectionEstablished()) - .retry(3, observable -> isConnectionEstablished()) - .subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - pingDisposable = d; - } - - @Override - public void onNext(GenericOverall genericOverall) { - - } - - @Override - public void onError(Throwable e) { - dispose(pingDisposable); - } - - @Override - public void onComplete() { - dispose(pingDisposable); - } - }); - } - - // Start pulling signaling messages - String urlToken = null; - if (isMultiSession) { - urlToken = roomToken; - } - - if (!conversationUser.hasSpreedFeatureCapability("no-ping") && !TextUtils.isEmpty( - roomId)) { - NotificationUtils.INSTANCE.cancelExistingNotificationsForRoom( - getApplicationContext(), conversationUser, roomId); - } else if (!TextUtils.isEmpty(roomToken)) { - NotificationUtils.INSTANCE.cancelExistingNotificationsForRoom( - getApplicationContext(), conversationUser, roomToken); - } - - if (!hasExternalSignalingServer) { - ncApi.pullSignalingMessages(credentials, - ApiUtils.getUrlForSignaling(baseUrl, urlToken)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .repeatWhen(observable -> observable) - .takeWhile(observable -> isConnectionEstablished()) - .retry(3, observable -> isConnectionEstablished()) - .subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - signalingDisposable = d; - } - - @Override - public void onNext(SignalingOverall signalingOverall) { - if (signalingOverall.getOcs().getSignalings() != null) { - for (int i = 0; i < signalingOverall.getOcs().getSignalings().size(); - i++) { - try { - receivedSignalingMessage( - signalingOverall.getOcs().getSignalings().get(i)); - } catch (IOException e) { - Log.e(TAG, "Failed to process received signaling" + - " message"); - } - } - } - } - - @Override - public void onError(Throwable e) { - dispose(signalingDisposable); - } - - @Override - public void onComplete() { - dispose(signalingDisposable); - } - }); - } - } - } - - @Override - public void onError(Throwable e) { - } - - @Override - public void onComplete() { - - } - }); - } - - private void setupAndInitiateWebSocketsConnection() { - if (webSocketConnectionHelper == null) { - webSocketConnectionHelper = new WebSocketConnectionHelper(); - } - - if (webSocketClient == null) { - webSocketClient = WebSocketConnectionHelper.getExternalSignalingInstanceForServer( - externalSignalingServer.getExternalSignalingServer(), - conversationUser, externalSignalingServer.getExternalSignalingTicket(), - TextUtils.isEmpty(credentials)); - } else { - if (webSocketClient.isConnected() && currentCallStatus.equals(CallStatus.PUBLISHER_FAILED)) { - webSocketClient.restartWebSocket(); - } - } - - joinRoomAndCall(); - } - - private void initiateCall() { - if (!TextUtils.isEmpty(roomToken)) { - checkPermissions(); - } else { - handleFromNotification(); - } - } - - @Override - protected void onDetach(@NonNull View view) { - eventBus.unregister(this); - super.onDetach(view); - } - - @Override - protected void onAttach(@NonNull View view) { - super.onAttach(view); - eventBus.register(this); - } - - @Subscribe(threadMode = ThreadMode.BACKGROUND) - public void onMessageEvent(WebSocketCommunicationEvent webSocketCommunicationEvent) { - switch (webSocketCommunicationEvent.getType()) { - case "hello": - if (!webSocketCommunicationEvent.getHashMap().containsKey("oldResumeId")) { - if (currentCallStatus.equals(CallStatus.RECONNECTING)) { - hangup(false); - } else { - initiateCall(); - } - } else { - } - break; - case "roomJoined": - startSendingNick(); - - if (webSocketCommunicationEvent.getHashMap().get("roomToken").equals(roomToken)) { - performCall(); - } - break; - case "participantsUpdate": - if (webSocketCommunicationEvent.getHashMap().get("roomToken").equals(roomToken)) { - processUsersInRoom((List>) webSocketClient.getJobWithId( - Integer.valueOf(webSocketCommunicationEvent.getHashMap().get("jobId")))); - } - break; - case "signalingMessage": - processMessage((NCSignalingMessage) webSocketClient.getJobWithId( - Integer.valueOf(webSocketCommunicationEvent.getHashMap().get("jobId")))); - break; - case "peerReadyForRequestingOffer": - webSocketClient.requestOfferForSessionIdWithType( - webSocketCommunicationEvent.getHashMap().get("sessionId"), "video"); - break; - } - } - - @OnClick({ R.id.pip_video_view, R.id.remote_renderers_layout }) - public void showCallControls() { - animateCallControls(true, 0); - } - - private void dispose(@Nullable Disposable disposable) { - if (disposable != null && !disposable.isDisposed()) { - disposable.dispose(); - } else if (disposable == null) { - - if (pingDisposable != null && !pingDisposable.isDisposed()) { - pingDisposable.dispose(); - pingDisposable = null; - } - - if (signalingDisposable != null && !signalingDisposable.isDisposed()) { - signalingDisposable.dispose(); - signalingDisposable = null; - } - } - } - - private void receivedSignalingMessage(Signaling signaling) throws IOException { - String messageType = signaling.getType(); - - if (!isConnectionEstablished() && !currentCallStatus.equals(CallStatus.CALLING)) { - return; - } - - if ("usersInRoom".equals(messageType)) { - processUsersInRoom((List>) signaling.getMessageWrapper()); - } else if ("message".equals(messageType)) { - NCSignalingMessage ncSignalingMessage = - LoganSquare.parse(signaling.getMessageWrapper().toString(), - NCSignalingMessage.class); - processMessage(ncSignalingMessage); - } else { - Log.d(TAG, "Something went very very wrong"); - } - } - - private void processMessage(NCSignalingMessage ncSignalingMessage) { - if (ncSignalingMessage.getRoomType().equals("video") || ncSignalingMessage.getRoomType() - .equals("screen")) { - MagicPeerConnectionWrapper magicPeerConnectionWrapper = - getPeerConnectionWrapperForSessionIdAndType(ncSignalingMessage.getFrom(), - ncSignalingMessage.getRoomType(), false); - - String type = null; - if (ncSignalingMessage.getPayload() != null - && ncSignalingMessage.getPayload().getType() != null) { - type = ncSignalingMessage.getPayload().getType(); - } else if (ncSignalingMessage.getType() != null) { - type = ncSignalingMessage.getType(); - } - - if (type != null) { - switch (type) { - case "unshareScreen": - endPeerConnection(ncSignalingMessage.getFrom(), true); - break; - case "offer": - case "answer": - magicPeerConnectionWrapper.setNick(ncSignalingMessage.getPayload().getNick()); - SessionDescription sessionDescriptionWithPreferredCodec; - - String sessionDescriptionStringWithPreferredCodec = MagicWebRTCUtils.preferCodec - (ncSignalingMessage.getPayload().getSdp(), - "H264", false); - - sessionDescriptionWithPreferredCodec = new SessionDescription( - SessionDescription.Type.fromCanonicalForm(type), - sessionDescriptionStringWithPreferredCodec); - - if (magicPeerConnectionWrapper.getPeerConnection() != null) { - magicPeerConnectionWrapper.getPeerConnection() - .setRemoteDescription(magicPeerConnectionWrapper - .getMagicSdpObserver(), sessionDescriptionWithPreferredCodec); - } - break; - case "candidate": - NCIceCandidate ncIceCandidate = ncSignalingMessage.getPayload().getIceCandidate(); - IceCandidate iceCandidate = new IceCandidate(ncIceCandidate.getSdpMid(), - ncIceCandidate.getSdpMLineIndex(), ncIceCandidate.getCandidate()); - magicPeerConnectionWrapper.addCandidate(iceCandidate); - break; - case "endOfCandidates": - magicPeerConnectionWrapper.drainIceCandidates(); - break; - default: - break; - } - } - } else { - Log.d(TAG, "Something went very very wrong"); - } - } - - private void hangup(boolean shutDownView) { - stopCallingSound(); - dispose(null); - - if (shutDownView) { - - if (videoCapturer != null) { - try { - videoCapturer.stopCapture(); - } catch (InterruptedException e) { - Log.e(TAG, "Failed to stop capturing while hanging up"); - } - videoCapturer.dispose(); - videoCapturer = null; - } - - if (pipVideoView != null) { - pipVideoView.release(); - } - - if (audioSource != null) { - audioSource.dispose(); - audioSource = null; - } - - if (audioManager != null) { - audioManager.stop(); - audioManager = null; - } - - if (videoSource != null) { - videoSource = null; - } - - if (peerConnectionFactory != null) { - peerConnectionFactory = null; - } - - localMediaStream = null; - localAudioTrack = null; - localVideoTrack = null; - - if (TextUtils.isEmpty(credentials) && hasExternalSignalingServer) { - WebSocketConnectionHelper.deleteExternalSignalingInstanceForUserEntity(-1); - } - } - - for (int i = 0; i < magicPeerConnectionWrapperList.size(); i++) { - endPeerConnection(magicPeerConnectionWrapperList.get(i).getSessionId(), false); - } - - hangupNetworkCalls(shutDownView); - } - - private void hangupNetworkCalls(boolean shutDownView) { - ncApi.leaveCall(credentials, ApiUtils.getUrlForCall(baseUrl, roomToken)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - - } - - @Override - public void onNext(GenericOverall genericOverall) { - if (!TextUtils.isEmpty(credentials) && hasExternalSignalingServer) { - webSocketClient.joinRoomWithRoomTokenAndSession("", callSession); - } - - if (isMultiSession) { - if (shutDownView && getActivity() != null) { - getActivity().finish(); - } else if (!shutDownView && (currentCallStatus.equals(CallStatus.RECONNECTING) - || currentCallStatus.equals(CallStatus.PUBLISHER_FAILED))) { - initiateCall(); - } - } else { - leaveRoom(shutDownView); - } - } - - @Override - public void onError(Throwable e) { - - } - - @Override - public void onComplete() { - - } - }); - } - - private void leaveRoom(boolean shutDownView) { - ncApi.leaveRoom(credentials, - ApiUtils.getUrlForSettingMyselfAsActiveParticipant(baseUrl, roomToken)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - - } - - @Override - public void onNext(GenericOverall genericOverall) { - if (shutDownView && getActivity() != null) { - getActivity().finish(); - } - } - - @Override - public void onError(Throwable e) { - - } - - @Override - public void onComplete() { - - } - }); - } - - private void startVideoCapture() { - if (videoCapturer != null) { - videoCapturer.startCapture(1280, 720, 30); - } - } - - private void processUsersInRoom(List> users) { - List newSessions = new ArrayList<>(); - Set oldSesssions = new HashSet<>(); - - for (HashMap participant : users) { - if (!participant.get("sessionId").equals(callSession)) { - Object inCallObject = participant.get("inCall"); - boolean isNewSession; - if (inCallObject instanceof Boolean) { - isNewSession = (boolean) inCallObject; - } else { - isNewSession = ((long) inCallObject) != 0; - } - - if (isNewSession) { - newSessions.add(participant.get("sessionId").toString()); - } else { - oldSesssions.add(participant.get("sessionId").toString()); - } - } - } - - for (MagicPeerConnectionWrapper magicPeerConnectionWrapper : magicPeerConnectionWrapperList) { - if (!magicPeerConnectionWrapper.isMCUPublisher()) { - oldSesssions.add(magicPeerConnectionWrapper.getSessionId()); - } - } - - // Calculate sessions that left the call - oldSesssions.removeAll(newSessions); - - // Calculate sessions that join the call - newSessions.removeAll(oldSesssions); - - if (!isConnectionEstablished() && !currentCallStatus.equals(CallStatus.CALLING)) { - return; - } - - if (newSessions.size() > 0 && !hasMCU) { - getPeersForCall(); - } - - hasMCU = hasExternalSignalingServer && webSocketClient != null && webSocketClient.hasMCU(); - - for (String sessionId : newSessions) { - getPeerConnectionWrapperForSessionIdAndType(sessionId, "video", - hasMCU && sessionId.equals(webSocketClient.getSessionId())); - } - - if (newSessions.size() > 0 && !currentCallStatus.equals(CallStatus.IN_CONVERSATION)) { - setCallState(CallStatus.IN_CONVERSATION); - } - - for (String sessionId : oldSesssions) { - endPeerConnection(sessionId, false); - } - } - - private void getPeersForCall() { - ncApi.getPeersForCall(credentials, ApiUtils.getUrlForCall(baseUrl, roomToken)) - .subscribeOn(Schedulers.io()) - .subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - - } - - @Override - public void onNext(ParticipantsOverall participantsOverall) { - participantMap = new HashMap<>(); - for (Participant participant : participantsOverall.getOcs().getData()) { - participantMap.put(participant.getSessionId(), participant); - if (getActivity() != null) { - getActivity().runOnUiThread( - () -> setupAvatarForSession(participant.getSessionId())); - } - } - } - - @Override - public void onError(Throwable e) { - - } - - @Override - public void onComplete() { - - } - }); - } - - private void deleteMagicPeerConnection(MagicPeerConnectionWrapper magicPeerConnectionWrapper) { - magicPeerConnectionWrapper.removePeerConnection(); - magicPeerConnectionWrapperList.remove(magicPeerConnectionWrapper); - } - - private MagicPeerConnectionWrapper getPeerConnectionWrapperForSessionId(String sessionId, - String type) { - for (int i = 0; i < magicPeerConnectionWrapperList.size(); i++) { - if (magicPeerConnectionWrapperList.get(i).getSessionId().equals(sessionId) - && magicPeerConnectionWrapperList.get(i).getVideoStreamType().equals(type)) { - return magicPeerConnectionWrapperList.get(i); - } - } - - return null; - } - - private MagicPeerConnectionWrapper getPeerConnectionWrapperForSessionIdAndType(String sessionId, - String type, boolean publisher) { - MagicPeerConnectionWrapper magicPeerConnectionWrapper; - if ((magicPeerConnectionWrapper = getPeerConnectionWrapperForSessionId(sessionId, type)) - != null) { - return magicPeerConnectionWrapper; - } else { - if (hasMCU && publisher) { - magicPeerConnectionWrapper = new MagicPeerConnectionWrapper(peerConnectionFactory, - iceServers, sdpConstraintsForMCU, sessionId, callSession, localMediaStream, true, true, - type); - } else if (hasMCU) { - magicPeerConnectionWrapper = new MagicPeerConnectionWrapper(peerConnectionFactory, - iceServers, sdpConstraints, sessionId, callSession, null, false, true, type); - } else { - if (!"screen".equals(type)) { - magicPeerConnectionWrapper = new MagicPeerConnectionWrapper(peerConnectionFactory, - iceServers, sdpConstraints, sessionId, callSession, localMediaStream, false, false, - type); - } else { - magicPeerConnectionWrapper = new MagicPeerConnectionWrapper(peerConnectionFactory, - iceServers, sdpConstraints, sessionId, callSession, null, false, false, type); - } - } - - magicPeerConnectionWrapperList.add(magicPeerConnectionWrapper); - - if (publisher) { - startSendingNick(); - } - - return magicPeerConnectionWrapper; - } - } - - private List getPeerConnectionWrapperListForSessionId( - String sessionId) { - List internalList = new ArrayList<>(); - for (MagicPeerConnectionWrapper magicPeerConnectionWrapper : magicPeerConnectionWrapperList) { - if (magicPeerConnectionWrapper.getSessionId().equals(sessionId)) { - internalList.add(magicPeerConnectionWrapper); - } - } - - return internalList; - } - - private void endPeerConnection(String sessionId, boolean justScreen) { - List magicPeerConnectionWrappers; - MagicPeerConnectionWrapper magicPeerConnectionWrapper; - if (!(magicPeerConnectionWrappers = - getPeerConnectionWrapperListForSessionId(sessionId)).isEmpty() - && getActivity() != null) { - for (int i = 0; i < magicPeerConnectionWrappers.size(); i++) { - magicPeerConnectionWrapper = magicPeerConnectionWrappers.get(i); - if (magicPeerConnectionWrapper.getSessionId().equals(sessionId)) { - if (magicPeerConnectionWrapper.getVideoStreamType().equals("screen") || !justScreen) { - MagicPeerConnectionWrapper finalMagicPeerConnectionWrapper = magicPeerConnectionWrapper; - getActivity().runOnUiThread(() -> removeMediaStream(sessionId + "+" + - finalMagicPeerConnectionWrapper.getVideoStreamType())); - deleteMagicPeerConnection(magicPeerConnectionWrapper); - } - } - } - } - } - - private void removeMediaStream(String sessionId) { - if (remoteRenderersLayout != null && remoteRenderersLayout.getChildCount() > 0) { - RelativeLayout relativeLayout = remoteRenderersLayout.findViewWithTag(sessionId); - if (relativeLayout != null) { - SurfaceViewRenderer surfaceViewRenderer = relativeLayout.findViewById(R.id.surface_view); - surfaceViewRenderer.release(); - remoteRenderersLayout.removeView(relativeLayout); - remoteRenderersLayout.invalidate(); - } - } - - if (callControls != null) { - callControls.setZ(100.0f); - } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onMessageEvent(ConfigurationChangeEvent configurationChangeEvent) { - powerManagerUtils.setOrientation( - Objects.requireNonNull(getResources()).getConfiguration().orientation); - - if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { - remoteRenderersLayout.setOrientation(LinearLayout.HORIZONTAL); - } else if (getResources().getConfiguration().orientation - == Configuration.ORIENTATION_PORTRAIT) { - remoteRenderersLayout.setOrientation(LinearLayout.VERTICAL); - } - - setPipVideoViewDimensions(); - - cookieManager.getCookieStore().removeAll(); - } - - private void setPipVideoViewDimensions() { - FrameLayout.LayoutParams layoutParams = - (FrameLayout.LayoutParams) pipVideoView.getLayoutParams(); - - if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { - remoteRenderersLayout.setOrientation(LinearLayout.HORIZONTAL); - layoutParams.height = (int) getResources().getDimension(R.dimen.large_preview_dimension); - layoutParams.width = FrameLayout.LayoutParams.WRAP_CONTENT; - pipVideoView.setLayoutParams(layoutParams); - } else if (getResources().getConfiguration().orientation - == Configuration.ORIENTATION_PORTRAIT) { - remoteRenderersLayout.setOrientation(LinearLayout.VERTICAL); - layoutParams.height = FrameLayout.LayoutParams.WRAP_CONTENT; - layoutParams.width = (int) getResources().getDimension(R.dimen.large_preview_dimension); - pipVideoView.setLayoutParams(layoutParams); - } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onMessageEvent(PeerConnectionEvent peerConnectionEvent) { - if (peerConnectionEvent.getPeerConnectionEventType() - .equals(PeerConnectionEvent.PeerConnectionEventType - .PEER_CLOSED)) { - endPeerConnection(peerConnectionEvent.getSessionId(), - peerConnectionEvent.getVideoStreamType().equals("screen")); - } else if (peerConnectionEvent.getPeerConnectionEventType().equals(PeerConnectionEvent - .PeerConnectionEventType.SENSOR_FAR) || - peerConnectionEvent.getPeerConnectionEventType().equals(PeerConnectionEvent - .PeerConnectionEventType.SENSOR_NEAR)) { - - if (!isVoiceOnlyCall) { - boolean enableVideo = - peerConnectionEvent.getPeerConnectionEventType().equals(PeerConnectionEvent - .PeerConnectionEventType.SENSOR_FAR) && videoOn; - if (getActivity() != null && EffortlessPermissions.hasPermissions(getActivity(), - PERMISSIONS_CAMERA) && - (currentCallStatus.equals(CallStatus.CALLING) || isConnectionEstablished()) && videoOn - && enableVideo != localVideoTrack.enabled()) { - toggleMedia(enableVideo, true); - } - } - } else if (peerConnectionEvent.getPeerConnectionEventType().equals(PeerConnectionEvent - .PeerConnectionEventType.NICK_CHANGE)) { - gotNick(peerConnectionEvent.getSessionId(), peerConnectionEvent.getNick(), true, - peerConnectionEvent.getVideoStreamType()); - } else if (peerConnectionEvent.getPeerConnectionEventType().equals(PeerConnectionEvent - .PeerConnectionEventType.VIDEO_CHANGE) && !isVoiceOnlyCall) { - gotAudioOrVideoChange(true, - peerConnectionEvent.getSessionId() + "+" + peerConnectionEvent.getVideoStreamType(), - peerConnectionEvent.getChangeValue()); - } else if (peerConnectionEvent.getPeerConnectionEventType().equals(PeerConnectionEvent - .PeerConnectionEventType.AUDIO_CHANGE)) { - gotAudioOrVideoChange(false, - peerConnectionEvent.getSessionId() + "+" + peerConnectionEvent.getVideoStreamType(), - peerConnectionEvent.getChangeValue()); - } else if (peerConnectionEvent.getPeerConnectionEventType() - .equals(PeerConnectionEvent.PeerConnectionEventType.PUBLISHER_FAILED)) { - currentCallStatus = CallStatus.PUBLISHER_FAILED; - webSocketClient.clearResumeId(); - hangup(false); - } - } - - private void startSendingNick() { - DataChannelMessageNick dataChannelMessage = new DataChannelMessageNick(); - dataChannelMessage.setType("nickChanged"); - HashMap nickChangedPayload = new HashMap<>(); - nickChangedPayload.put("userid", conversationUser.getUserId()); - nickChangedPayload.put("name", conversationUser.getDisplayName()); - dataChannelMessage.setPayload(nickChangedPayload); - final MagicPeerConnectionWrapper magicPeerConnectionWrapper; - for (int i = 0; i < magicPeerConnectionWrapperList.size(); i++) { - if (magicPeerConnectionWrapperList.get(i).isMCUPublisher()) { - magicPeerConnectionWrapper = magicPeerConnectionWrapperList.get(i); - Observable - .interval(1, TimeUnit.SECONDS) - .repeatUntil(() -> (!isConnectionEstablished() || isBeingDestroyed() || isDestroyed())) - .observeOn(Schedulers.io()) - .subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - - } - - @Override - public void onNext(Long aLong) { - magicPeerConnectionWrapper.sendNickChannelData(dataChannelMessage); - } - - @Override - public void onError(Throwable e) { - - } - - @Override - public void onComplete() { - - } - }); - break; - } - } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onMessageEvent(MediaStreamEvent mediaStreamEvent) { - if (mediaStreamEvent.getMediaStream() != null) { - setupVideoStreamForLayout(mediaStreamEvent.getMediaStream(), mediaStreamEvent.getSession(), - mediaStreamEvent.getMediaStream().videoTracks != null - && mediaStreamEvent.getMediaStream().videoTracks.size() > 0, - mediaStreamEvent.getVideoStreamType()); - } else { - setupVideoStreamForLayout(null, mediaStreamEvent.getSession(), false, - mediaStreamEvent.getVideoStreamType()); - } - } - - @Subscribe(threadMode = ThreadMode.BACKGROUND) - public void onMessageEvent(SessionDescriptionSendEvent sessionDescriptionSend) - throws IOException { - NCMessageWrapper ncMessageWrapper = new NCMessageWrapper(); - ncMessageWrapper.setEv("message"); - ncMessageWrapper.setSessionId(callSession); - NCSignalingMessage ncSignalingMessage = new NCSignalingMessage(); - ncSignalingMessage.setTo(sessionDescriptionSend.getPeerId()); - ncSignalingMessage.setRoomType(sessionDescriptionSend.getVideoStreamType()); - ncSignalingMessage.setType(sessionDescriptionSend.getType()); - NCMessagePayload ncMessagePayload = new NCMessagePayload(); - ncMessagePayload.setType(sessionDescriptionSend.getType()); - - if (!"candidate".equals(sessionDescriptionSend.getType())) { - ncMessagePayload.setSdp(sessionDescriptionSend.getSessionDescription().description); - ncMessagePayload.setNick(conversationUser.getDisplayName()); - } else { - ncMessagePayload.setIceCandidate(sessionDescriptionSend.getNcIceCandidate()); - } - - // Set all we need - ncSignalingMessage.setPayload(ncMessagePayload); - ncMessageWrapper.setSignalingMessage(ncSignalingMessage); - - if (!hasExternalSignalingServer) { - StringBuilder stringBuilder = new StringBuilder(); - stringBuilder.append("{") - .append("\"fn\":\"") - .append(StringEscapeUtils.escapeJson( - LoganSquare.serialize(ncMessageWrapper.getSignalingMessage()))).append("\"") - .append(",") - .append("\"sessionId\":") - .append("\"").append(StringEscapeUtils.escapeJson(callSession)).append("\"") - .append(",") - .append("\"ev\":\"message\"") - .append("}"); - - List strings = new ArrayList<>(); - String stringToSend = stringBuilder.toString(); - strings.add(stringToSend); - - String urlToken = null; - if (isMultiSession) { - urlToken = roomToken; - } - - ncApi.sendSignalingMessages(credentials, ApiUtils.getUrlForSignaling(baseUrl, urlToken), - strings.toString()) - .retry(3) - .subscribeOn(Schedulers.io()) - .subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - - } - - @Override - public void onNext(SignalingOverall signalingOverall) { - if (signalingOverall.getOcs().getSignalings() != null) { - for (int i = 0; i < signalingOverall.getOcs().getSignalings().size(); i++) { - try { - receivedSignalingMessage(signalingOverall.getOcs().getSignalings().get(i)); - } catch (IOException e) { - e.printStackTrace(); - } - } - } - } - - @Override - public void onError(Throwable e) { - } - - @Override - public void onComplete() { - - } - }); - } else { - webSocketClient.sendCallMessage(ncMessageWrapper); - } - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, - @NonNull int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - - EffortlessPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, - this); - } - - private void setupAvatarForSession(String session) { - if (remoteRenderersLayout != null) { - RelativeLayout relativeLayout = remoteRenderersLayout.findViewWithTag(session + "+video"); - if (relativeLayout != null) { - SimpleDraweeView avatarImageView = relativeLayout.findViewById(R.id.avatarImageView); - - String userId; - - if (hasMCU) { - userId = webSocketClient.getUserIdForSession(session); - } else { - userId = participantMap.get(session).getUserId(); - } - - if (!TextUtils.isEmpty(userId)) { - - if (getActivity() != null) { - avatarImageView.setController(null); - - DraweeController draweeController = Fresco.newDraweeControllerBuilder() - .setOldController(avatarImageView.getController()) - .setImageRequest( - DisplayUtils.INSTANCE.getImageRequestForUrl(ApiUtils.getUrlForAvatarWithName(baseUrl, - userId, - R.dimen.avatar_size_big), null)) - .build(); - avatarImageView.setController(draweeController); - } - } - } - } - } - - private void setupVideoStreamForLayout(@Nullable MediaStream mediaStream, String session, - boolean enable, String videoStreamType) { - boolean isInitialLayoutSetupForPeer = false; - if (remoteRenderersLayout.findViewWithTag(session) == null) { - setupNewPeerLayout(session, videoStreamType); - isInitialLayoutSetupForPeer = true; - } - - RelativeLayout relativeLayout = - remoteRenderersLayout.findViewWithTag(session + "+" + videoStreamType); - SurfaceViewRenderer surfaceViewRenderer = relativeLayout.findViewById(R.id.surface_view); - SimpleDraweeView imageView = relativeLayout.findViewById(R.id.avatarImageView); - - if (mediaStream != null - && mediaStream.videoTracks != null - && mediaStream.videoTracks.size() > 0 - && enable) { - VideoTrack videoTrack = mediaStream.videoTracks.get(0); - - videoTrack.addSink(surfaceViewRenderer); - - imageView.setVisibility(View.INVISIBLE); - surfaceViewRenderer.setVisibility(View.VISIBLE); - } else { - imageView.setVisibility(View.VISIBLE); - surfaceViewRenderer.setVisibility(View.INVISIBLE); - - if (isInitialLayoutSetupForPeer && isVoiceOnlyCall) { - gotAudioOrVideoChange(true, session, false); - } - } - - callControls.setZ(100.0f); - } - - private void gotAudioOrVideoChange(boolean video, String sessionId, boolean change) { - RelativeLayout relativeLayout = remoteRenderersLayout.findViewWithTag(sessionId); - if (relativeLayout != null) { - ImageView imageView; - SimpleDraweeView avatarImageView = relativeLayout.findViewById(R.id.avatarImageView); - SurfaceViewRenderer surfaceViewRenderer = relativeLayout.findViewById(R.id.surface_view); - - if (video) { - imageView = relativeLayout.findViewById(R.id.remote_video_off); - - if (change) { - avatarImageView.setVisibility(View.INVISIBLE); - surfaceViewRenderer.setVisibility(View.VISIBLE); - } else { - avatarImageView.setVisibility(View.VISIBLE); - surfaceViewRenderer.setVisibility(View.INVISIBLE); - } - } else { - imageView = relativeLayout.findViewById(R.id.remote_audio_off); - } - - if (change && imageView.getVisibility() != View.INVISIBLE) { - imageView.setVisibility(View.INVISIBLE); - } else if (!change && imageView.getVisibility() != View.VISIBLE) { - imageView.setVisibility(View.VISIBLE); - } - } - } - - private void setupNewPeerLayout(String session, String type) { - if (remoteRenderersLayout.findViewWithTag(session + "+" + type) == null - && getActivity() != null) { - getActivity().runOnUiThread(() -> { - RelativeLayout relativeLayout = (RelativeLayout) - getActivity().getLayoutInflater().inflate(R.layout.call_item, remoteRenderersLayout, - false); - relativeLayout.setTag(session + "+" + type); - SurfaceViewRenderer surfaceViewRenderer = relativeLayout.findViewById(R.id - .surface_view); - - surfaceViewRenderer.setMirror(false); - surfaceViewRenderer.init(rootEglBase.getEglBaseContext(), null); - surfaceViewRenderer.setZOrderMediaOverlay(false); - // disabled because it causes some devices to crash - surfaceViewRenderer.setEnableHardwareScaler(false); - surfaceViewRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT); - surfaceViewRenderer.setOnClickListener(videoOnClickListener); - remoteRenderersLayout.addView(relativeLayout); - if (hasExternalSignalingServer) { - gotNick(session, webSocketClient.getDisplayNameForSession(session), false, type); - } else { - gotNick(session, - getPeerConnectionWrapperForSessionIdAndType(session, type, false).getNick(), false, - type); - } - - if ("video".equals(type)) { - setupAvatarForSession(session); - } - - callControls.setZ(100.0f); - }); - } - } - - private void gotNick(String sessionOrUserId, String nick, boolean isFromAnEvent, String type) { - if (isFromAnEvent && hasExternalSignalingServer) { - // get session based on userId - sessionOrUserId = webSocketClient.getSessionForUserId(sessionOrUserId); - } - - sessionOrUserId += "+" + type; - - if (relativeLayout != null) { - RelativeLayout relativeLayout = remoteRenderersLayout.findViewWithTag(sessionOrUserId); - TextView textView = relativeLayout.findViewById(R.id.peer_nick_text_view); - if (!textView.getText().equals(nick)) { - textView.setText(nick); - } - } - } - - @OnClick(R.id.connectingRelativeLayoutView) - public void onConnectingViewClick() { - if (currentCallStatus.equals(CallStatus.CALLING_TIMEOUT)) { - setCallState(CallStatus.RECONNECTING); - hangupNetworkCalls(false); - } - } - - private void setCallState(CallStatus callState) { - if (currentCallStatus == null || !currentCallStatus.equals(callState)) { - currentCallStatus = callState; - if (handler == null) { - handler = new Handler(Looper.getMainLooper()); - } else { - handler.removeCallbacksAndMessages(null); - } - - switch (callState) { - case CALLING: - handler.post(() -> { - playCallingSound(); - connectingTextView.setText(R.string.nc_connecting_call); - if (connectingView.getVisibility() != View.VISIBLE) { - connectingView.setVisibility(View.VISIBLE); - } - - if (conversationView.getVisibility() != View.INVISIBLE) { - conversationView.setVisibility(View.INVISIBLE); - } - - if (progressBar.getVisibility() != View.VISIBLE) { - progressBar.setVisibility(View.VISIBLE); - } - - if (errorImageView.getVisibility() != View.GONE) { - errorImageView.setVisibility(View.GONE); - } - }); - break; - case CALLING_TIMEOUT: - handler.post(() -> { - hangup(false); - connectingTextView.setText(R.string.nc_call_timeout); - if (connectingView.getVisibility() != View.VISIBLE) { - connectingView.setVisibility(View.VISIBLE); - } - - if (progressBar.getVisibility() != View.GONE) { - progressBar.setVisibility(View.GONE); - } - - if (conversationView.getVisibility() != View.INVISIBLE) { - conversationView.setVisibility(View.INVISIBLE); - } - - errorImageView.setImageResource(R.drawable.ic_av_timer_timer_24dp); - - if (errorImageView.getVisibility() != View.VISIBLE) { - errorImageView.setVisibility(View.VISIBLE); - } - }); - break; - case RECONNECTING: - handler.post(() -> { - playCallingSound(); - connectingTextView.setText(R.string.nc_call_reconnecting); - if (connectingView.getVisibility() != View.VISIBLE) { - connectingView.setVisibility(View.VISIBLE); - } - if (conversationView.getVisibility() != View.INVISIBLE) { - conversationView.setVisibility(View.INVISIBLE); - } - if (progressBar.getVisibility() != View.VISIBLE) { - progressBar.setVisibility(View.VISIBLE); - } - - if (errorImageView.getVisibility() != View.GONE) { - errorImageView.setVisibility(View.GONE); - } - }); - break; - case ESTABLISHED: - handler.postDelayed(() -> setCallState(CallStatus.CALLING_TIMEOUT), 45000); - handler.post(() -> { - if (connectingView != null) { - connectingTextView.setText(R.string.nc_calling); - if (connectingTextView.getVisibility() != View.VISIBLE) { - connectingView.setVisibility(View.VISIBLE); - } - } - - if (progressBar != null) { - if (progressBar.getVisibility() != View.VISIBLE) { - progressBar.setVisibility(View.VISIBLE); - } - } - - if (conversationView != null) { - if (conversationView.getVisibility() != View.INVISIBLE) { - conversationView.setVisibility(View.INVISIBLE); - } - } - - if (errorImageView != null) { - if (errorImageView.getVisibility() != View.GONE) { - errorImageView.setVisibility(View.GONE); - } - } - }); - break; - case IN_CONVERSATION: - handler.post(() -> { - stopCallingSound(); - - if (!isPTTActive) { - animateCallControls(false, 5000); - } - - if (connectingView != null) { - if (connectingView.getVisibility() != View.INVISIBLE) { - connectingView.setVisibility(View.INVISIBLE); - } - } - - if (progressBar != null) { - if (progressBar.getVisibility() != View.GONE) { - progressBar.setVisibility(View.GONE); - } - } - - if (conversationView != null) { - if (conversationView.getVisibility() != View.VISIBLE) { - conversationView.setVisibility(View.VISIBLE); - } - } - - if (errorImageView != null) { - if (errorImageView.getVisibility() != View.GONE) { - errorImageView.setVisibility(View.GONE); - } - } - }); - break; - case OFFLINE: - handler.post(() -> { - stopCallingSound(); - - if (connectingTextView != null) { - connectingTextView.setText(R.string.nc_offline); - - if (connectingView.getVisibility() != View.VISIBLE) { - connectingView.setVisibility(View.VISIBLE); - } - } - - if (conversationView != null) { - if (conversationView.getVisibility() != View.INVISIBLE) { - conversationView.setVisibility(View.INVISIBLE); - } - } - - if (progressBar != null) { - if (progressBar.getVisibility() != View.GONE) { - progressBar.setVisibility(View.GONE); - } - } - - if (errorImageView != null) { - errorImageView.setImageResource(R.drawable.ic_signal_wifi_off_white_24dp); - if (errorImageView.getVisibility() != View.VISIBLE) { - errorImageView.setVisibility(View.VISIBLE); - } - } - }); - break; - case LEAVING: - handler.post(() -> { - if (!isDestroyed() && !isBeingDestroyed()) { - stopCallingSound(); - connectingTextView.setText(R.string.nc_leaving_call); - connectingView.setVisibility(View.VISIBLE); - conversationView.setVisibility(View.INVISIBLE); - progressBar.setVisibility(View.VISIBLE); - errorImageView.setVisibility(View.GONE); - } - }); - break; - default: - } - } - } - - private void playCallingSound() { - stopCallingSound(); - Uri ringtoneUri = Uri.parse("android.resource://" - + getApplicationContext().getPackageName() - + "/raw/librem_by_feandesign_call"); - if (getActivity() != null) { - mediaPlayer = new MediaPlayer(); - try { - mediaPlayer.setDataSource(Objects.requireNonNull(getActivity()), ringtoneUri); - mediaPlayer.setLooping(true); - AudioAttributes audioAttributes = - new AudioAttributes.Builder().setContentType(AudioAttributes - .CONTENT_TYPE_SONIFICATION) - .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) - .build(); - mediaPlayer.setAudioAttributes(audioAttributes); - - mediaPlayer.setOnPreparedListener(mp -> mediaPlayer.start()); - - mediaPlayer.prepareAsync(); - } catch (IOException e) { - Log.e(TAG, "Failed to play sound"); - } - } - } - - private void stopCallingSound() { - if (mediaPlayer != null) { - if (mediaPlayer.isPlaying()) { - mediaPlayer.stop(); - } - - mediaPlayer.release(); - mediaPlayer = null; - } - } - - @Subscribe(threadMode = ThreadMode.BACKGROUND) - public void onMessageEvent(NetworkEvent networkEvent) { - if (networkEvent.getNetworkConnectionEvent() - .equals(NetworkEvent.NetworkConnectionEvent.NETWORK_CONNECTED)) { - if (handler != null) { - handler.removeCallbacksAndMessages(null); - } - - /*if (!hasMCU) { - setCallState(CallStatus.RECONNECTING); - hangupNetworkCalls(false); - }*/ - - } else if (networkEvent.getNetworkConnectionEvent() - .equals(NetworkEvent.NetworkConnectionEvent.NETWORK_DISCONNECTED)) { - if (handler != null) { - handler.removeCallbacksAndMessages(null); - } - - /* if (!hasMCU) { - setCallState(CallStatus.OFFLINE); - hangup(false); - }*/ - } - } - - @Parcel - public enum CallStatus { - CALLING, CALLING_TIMEOUT, ESTABLISHED, IN_CONVERSATION, RECONNECTING, OFFLINE, LEAVING, PUBLISHER_FAILED - } - - private class MicrophoneButtonTouchListener implements View.OnTouchListener { - - @SuppressLint("ClickableViewAccessibility") - @Override - public boolean onTouch(View v, MotionEvent event) { - v.onTouchEvent(event); - if (event.getAction() == MotionEvent.ACTION_UP && isPTTActive) { - isPTTActive = false; - microphoneControlButton.getHierarchy() - .setPlaceholderImage(R.drawable.ic_mic_off_white_24px); - pulseAnimation.stop(); - toggleMedia(false, false); - animateCallControls(false, 5000); - } - return true; - } - } - - private class VideoClickListener implements View.OnClickListener { - - @Override - public void onClick(View v) { - showCallControls(); - } - } -} diff --git a/app/src/main/java/com/nextcloud/talk/controllers/CallController.kt b/app/src/main/java/com/nextcloud/talk/controllers/CallController.kt new file mode 100644 index 000000000..d9c90b254 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/controllers/CallController.kt @@ -0,0 +1,2478 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * Copyright (C) 2017-2018 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.controllers + +import android.Manifest +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.annotation.SuppressLint +import android.content.res.Configuration +import android.graphics.Color +import android.media.AudioAttributes +import android.media.MediaPlayer +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.text.TextUtils +import android.util.Log +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.ProgressBar +import android.widget.RelativeLayout +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import autodagger.AutoInjector +import butterknife.BindView +import butterknife.OnClick +import butterknife.OnLongClick +import com.bluelinelabs.logansquare.LoganSquare +import com.facebook.drawee.backends.pipeline.Fresco +import com.facebook.drawee.interfaces.DraweeController +import com.facebook.drawee.view.SimpleDraweeView +import com.nextcloud.talk.R +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.controllers.base.BaseController +import com.nextcloud.talk.events.ConfigurationChangeEvent +import com.nextcloud.talk.events.MediaStreamEvent +import com.nextcloud.talk.events.NetworkEvent +import com.nextcloud.talk.events.PeerConnectionEvent +import com.nextcloud.talk.events.SessionDescriptionSendEvent +import com.nextcloud.talk.events.WebSocketCommunicationEvent +import com.nextcloud.talk.models.ExternalSignalingServer +import com.nextcloud.talk.models.json.capabilities.CapabilitiesOverall +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.participants.Participant +import com.nextcloud.talk.models.json.participants.ParticipantsOverall +import com.nextcloud.talk.models.json.signaling.DataChannelMessage +import com.nextcloud.talk.models.json.signaling.DataChannelMessageNick +import com.nextcloud.talk.models.json.signaling.NCIceCandidate +import com.nextcloud.talk.models.json.signaling.NCMessagePayload +import com.nextcloud.talk.models.json.signaling.NCMessageWrapper +import com.nextcloud.talk.models.json.signaling.NCSignalingMessage +import com.nextcloud.talk.models.json.signaling.Signaling +import com.nextcloud.talk.models.json.signaling.SignalingOverall +import com.nextcloud.talk.models.json.signaling.settings.IceServer +import com.nextcloud.talk.models.json.signaling.settings.SignalingSettingsOverall +import com.nextcloud.talk.newarch.local.models.UserNgEntity +import com.nextcloud.talk.newarch.local.models.hasSpreedFeatureCapability +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.DisplayUtils +import com.nextcloud.talk.utils.NotificationUtils +import com.nextcloud.talk.utils.animations.PulseAnimation +import com.nextcloud.talk.utils.bundle.BundleKeys +import com.nextcloud.talk.utils.database.user.UserUtils +import com.nextcloud.talk.utils.power.PowerManagerUtils +import com.nextcloud.talk.utils.preferences.AppPreferences +import com.nextcloud.talk.webrtc.MagicAudioManager +import com.nextcloud.talk.webrtc.MagicPeerConnectionWrapper +import com.nextcloud.talk.webrtc.MagicWebRTCUtils +import com.nextcloud.talk.webrtc.MagicWebSocketInstance +import com.nextcloud.talk.webrtc.WebSocketConnectionHelper +import com.wooplr.spotlight.SpotlightView +import io.reactivex.Observable +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import java.io.IOException +import java.net.CookieManager +import java.util.ArrayList +import java.util.HashMap +import java.util.HashSet +import java.util.Objects +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import me.zhanghai.android.effortlesspermissions.AfterPermissionDenied +import me.zhanghai.android.effortlesspermissions.EffortlessPermissions +import me.zhanghai.android.effortlesspermissions.OpenAppDetailsDialogFragment +import org.apache.commons.lang3.StringEscapeUtils +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import org.parceler.Parcel +import org.webrtc.AudioSource +import org.webrtc.AudioTrack +import org.webrtc.Camera1Enumerator +import org.webrtc.Camera2Enumerator +import org.webrtc.CameraEnumerator +import org.webrtc.CameraVideoCapturer +import org.webrtc.EglBase +import org.webrtc.IceCandidate +import org.webrtc.Logging +import org.webrtc.MediaConstraints +import org.webrtc.MediaStream +import org.webrtc.PeerConnection +import org.webrtc.PeerConnectionFactory +import org.webrtc.RendererCommon +import org.webrtc.SessionDescription +import org.webrtc.SurfaceViewRenderer +import org.webrtc.VideoCapturer +import org.webrtc.VideoSource +import org.webrtc.VideoTrack +import pub.devrel.easypermissions.AfterPermissionGranted + +@AutoInjector(NextcloudTalkApplication::class) +class CallController(args: Bundle) : BaseController() { + + @JvmField + @BindView(R.id.callControlEnableSpeaker) + var callControlEnableSpeaker: SimpleDraweeView? = null + @JvmField + @BindView(R.id.pip_video_view) + var pipVideoView: SurfaceViewRenderer? = null + @JvmField + @BindView(R.id.relative_layout) + var relativeLayout: RelativeLayout? = null + @JvmField + @BindView(R.id.remote_renderers_layout) + var remoteRenderersLayout: LinearLayout? = null + + @JvmField + @BindView(R.id.callControlsRelativeLayout) + var callControls: RelativeLayout? = null + @JvmField + @BindView(R.id.call_control_microphone) + var microphoneControlButton: SimpleDraweeView? = null + @JvmField + @BindView(R.id.call_control_camera) + var cameraControlButton: SimpleDraweeView? = null + @JvmField + @BindView(R.id.call_control_switch_camera) + var cameraSwitchButton: SimpleDraweeView? = null + @JvmField + @BindView(R.id.connectingTextView) + var connectingTextView: TextView? = null + + @JvmField + @BindView(R.id.connectingRelativeLayoutView) + var connectingView: RelativeLayout? = null + + @JvmField + @BindView(R.id.conversationRelativeLayoutView) + var conversationView: RelativeLayout? = null + + @JvmField + @BindView(R.id.errorImageView) + var errorImageView: ImageView? = null + + @JvmField + @BindView(R.id.progress_bar) + var progressBar: ProgressBar? = null + + @JvmField + @Inject + var ncApi: NcApi? = null + @JvmField + @Inject + var userUtils: UserUtils? = null + @JvmField + @Inject + var cookieManager: CookieManager? = null + + private var peerConnectionFactory: PeerConnectionFactory? = null + private var audioConstraints: MediaConstraints? = null + private var videoConstraints: MediaConstraints? = null + private var sdpConstraints: MediaConstraints? = null + private var sdpConstraintsForMCU: MediaConstraints? = null + private var audioManager: MagicAudioManager? = null + private var videoSource: VideoSource? = null + private var localVideoTrack: VideoTrack? = null + private var audioSource: AudioSource? = null + private var localAudioTrack: AudioTrack? = null + private var videoCapturer: VideoCapturer? = null + private var rootEglBase: EglBase? = null + private var signalingDisposable: Disposable? = null + private var pingDisposable: Disposable? = null + private var iceServers: MutableList? = null + private var cameraEnumerator: CameraEnumerator? = null + private var roomToken: String? = null + private val conversationUser: UserNgEntity? + private var callSession: String? = null + private var localMediaStream: MediaStream? = null + private val credentials: String? + private val magicPeerConnectionWrapperList = ArrayList() + private var participantMap: MutableMap = HashMap() + + private var videoOn = false + private var audioOn = false + + private var isMultiSession = false + private var needsPing = true + + private val isVoiceOnlyCall: Boolean + private val callControlHandler = Handler() + private val cameraSwitchHandler = Handler() + + private var isPTTActive = false + private var pulseAnimation: PulseAnimation? = null + private var videoOnClickListener: View.OnClickListener? = null + + private var baseUrl: String? = null + private val roomId: String + + private var spotlightView: SpotlightView? = null + + private var externalSignalingServer: ExternalSignalingServer? = null + private var webSocketClient: MagicWebSocketInstance? = null + private var webSocketConnectionHelper: WebSocketConnectionHelper? = null + private var hasMCU: Boolean = false + private var hasExternalSignalingServer: Boolean = false + private val conversationPassword: String + + private val powerManagerUtils: PowerManagerUtils + + private var handler: Handler? = null + + private var currentCallStatus: CallStatus? = null + + private var mediaPlayer: MediaPlayer? = null + + private val isConnectionEstablished: Boolean + get() = currentCallStatus == CallStatus.ESTABLISHED || currentCallStatus == CallStatus.IN_CONVERSATION + + init { + NextcloudTalkApplication.sharedApplication!! + .componentApplication + .inject(this) + + roomId = args.getString(BundleKeys.KEY_ROOM_ID, "") + roomToken = args.getString(BundleKeys.KEY_ROOM_TOKEN, "") + conversationUser = args.getParcelable(BundleKeys.KEY_USER_ENTITY) + conversationPassword = args.getString(BundleKeys.KEY_CONVERSATION_PASSWORD, "") + isVoiceOnlyCall = args.getBoolean(BundleKeys.KEY_CALL_VOICE_ONLY, false) + + credentials = ApiUtils.getCredentials(conversationUser!!.username, conversationUser.token) + + baseUrl = args.getString(BundleKeys.KEY_MODIFIED_BASE_URL, "") + + if (TextUtils.isEmpty(baseUrl)) { + baseUrl = conversationUser.baseUrl + } + + powerManagerUtils = PowerManagerUtils() + setCallState(CallStatus.CALLING) + } + + override fun inflateView( + inflater: LayoutInflater, + container: ViewGroup + ): View { + return inflater.inflate(R.layout.controller_call, container, false) + } + + private fun createCameraEnumerator() { + if (activity != null) { + var camera2EnumeratorIsSupported = false + try { + camera2EnumeratorIsSupported = Camera2Enumerator.isSupported(activity) + } catch (throwable: Throwable) { + Log.w(TAG, "Camera2Enumator threw an error") + } + + if (camera2EnumeratorIsSupported) { + cameraEnumerator = Camera2Enumerator(activity!!) + } else { + cameraEnumerator = + Camera1Enumerator(MagicWebRTCUtils.shouldEnableVideoHardwareAcceleration()) + } + } + } + + override fun onViewBound(view: View) { + super.onViewBound(view) + + microphoneControlButton!!.setOnTouchListener(MicrophoneButtonTouchListener()) + videoOnClickListener = VideoClickListener() + + pulseAnimation = PulseAnimation.create() + .with(microphoneControlButton!!) + .setDuration(310) + .setRepeatCount(PulseAnimation.INFINITE) + .setRepeatMode(PulseAnimation.REVERSE) + + setPipVideoViewDimensions() + + callControls!!.z = 100.0f + basicInitialization() + initViews() + + initiateCall() + } + + private fun basicInitialization() { + rootEglBase = EglBase.create() + createCameraEnumerator() + + //Create a new PeerConnectionFactory instance. + val options = PeerConnectionFactory.Options() + peerConnectionFactory = PeerConnectionFactory.builder() + .createPeerConnectionFactory() + + peerConnectionFactory!!.setVideoHwAccelerationOptions( + rootEglBase!!.eglBaseContext, + rootEglBase!!.eglBaseContext + ) + + //Create MediaConstraints - Will be useful for specifying video and audio constraints. + audioConstraints = MediaConstraints() + videoConstraints = MediaConstraints() + + localMediaStream = peerConnectionFactory!!.createLocalMediaStream("NCMS") + + // Create and audio manager that will take care of audio routing, + // audio modes, audio device enumeration etc. + audioManager = MagicAudioManager.create(applicationContext, !isVoiceOnlyCall) + // Store existing audio settings and change audio mode to + // MODE_IN_COMMUNICATION for best possible VoIP performance. + Log.d(TAG, "Starting the audio manager...") + audioManager!!.start( + MagicAudioManager.AudioManagerEvents { device, availableDevices -> + this.onAudioManagerDevicesChanged( + device, availableDevices + ) + }) + + iceServers = mutableListOf() + + //create sdpConstraints + sdpConstraints = MediaConstraints() + sdpConstraintsForMCU = MediaConstraints() + sdpConstraints!!.mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true")) + var offerToReceiveVideoString = "true" + + if (isVoiceOnlyCall) { + offerToReceiveVideoString = "false" + } + + sdpConstraints!!.mandatory.add( + MediaConstraints.KeyValuePair("OfferToReceiveVideo", offerToReceiveVideoString) + ) + + sdpConstraintsForMCU!!.mandatory.add( + MediaConstraints.KeyValuePair("OfferToReceiveAudio", "false") + ) + sdpConstraintsForMCU!!.mandatory.add( + MediaConstraints.KeyValuePair("OfferToReceiveVideo", "false") + ) + + sdpConstraintsForMCU!!.optional.add( + MediaConstraints.KeyValuePair("internalSctpDataChannels", "true") + ) + sdpConstraintsForMCU!!.optional.add( + MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true") + ) + + sdpConstraints!!.optional.add( + MediaConstraints.KeyValuePair("internalSctpDataChannels", "true") + ) + sdpConstraints!!.optional.add(MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true")) + + if (!isVoiceOnlyCall) { + cameraInitialization() + } + + microphoneInitialization() + } + + private fun handleFromNotification() { + ncApi!!.getRooms(credentials, ApiUtils.getUrlForGetRooms(baseUrl)) + .retry(3) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + + } + + override fun onNext(roomsOverall: RoomsOverall) { + for (conversation in roomsOverall.ocs.data) { + if (roomId == conversation.conversationId) { + roomToken = conversation.token + break + } + } + + checkPermissions() + } + + override fun onError(e: Throwable) { + + } + + override fun onComplete() { + + } + }) + } + + private fun initViews() { + if (isVoiceOnlyCall) { + callControlEnableSpeaker!!.visibility = View.VISIBLE + cameraSwitchButton!!.visibility = View.GONE + cameraControlButton!!.visibility = View.GONE + pipVideoView!!.visibility = View.GONE + } else { + if (cameraEnumerator!!.deviceNames.size < 2) { + cameraSwitchButton!!.visibility = View.GONE + } + + pipVideoView!!.init(rootEglBase!!.eglBaseContext, null) + pipVideoView!!.setZOrderMediaOverlay(true) + // disabled because it causes some devices to crash + pipVideoView!!.setEnableHardwareScaler(false) + pipVideoView!!.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT) + } + } + + private fun checkPermissions() { + if (isVoiceOnlyCall) { + onMicrophoneClick() + } else if (activity != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + requestPermissions(PERMISSIONS_CALL, 100) + } else { + onRequestPermissionsResult(100, PERMISSIONS_CALL, intArrayOf(1, 1)) + } + } + } + + @AfterPermissionGranted(100) + private fun onPermissionsGranted() { + if (EffortlessPermissions.hasPermissions(activity, *PERMISSIONS_CALL)) { + if (!videoOn && !isVoiceOnlyCall) { + onCameraClick() + } + + if (!audioOn) { + onMicrophoneClick() + } + + if (!isVoiceOnlyCall) { + if (cameraEnumerator!!.deviceNames.size == 0) { + cameraControlButton!!.visibility = View.GONE + } + + if (cameraEnumerator!!.deviceNames.size > 1) { + cameraSwitchButton!!.visibility = View.VISIBLE + } + } + + if (!isConnectionEstablished) { + fetchSignalingSettings() + } + } else if (activity != null && EffortlessPermissions.somePermissionPermanentlyDenied( + activity!!, + *PERMISSIONS_CALL + ) + ) { + checkIfSomeAreApproved() + } + } + + private fun checkIfSomeAreApproved() { + if (!isVoiceOnlyCall) { + if (cameraEnumerator!!.deviceNames.size == 0) { + cameraControlButton!!.visibility = View.GONE + } + + if (cameraEnumerator!!.deviceNames.size > 1) { + cameraSwitchButton!!.visibility = View.VISIBLE + } + + if (activity != null && EffortlessPermissions.hasPermissions( + activity, + *PERMISSIONS_CAMERA + ) + ) { + if (!videoOn) { + onCameraClick() + } + } else { + cameraControlButton!!.hierarchy + .setPlaceholderImage(R.drawable.ic_videocam_off_white_24px) + cameraControlButton!!.alpha = 0.7f + cameraSwitchButton!!.visibility = View.GONE + } + } + + if (EffortlessPermissions.hasPermissions(activity, *PERMISSIONS_MICROPHONE)) { + if (!audioOn) { + onMicrophoneClick() + } + } else { + microphoneControlButton!!.hierarchy.setPlaceholderImage(R.drawable.ic_mic_off_white_24px) + } + + if (!isConnectionEstablished) { + fetchSignalingSettings() + } + } + + @AfterPermissionDenied(100) + private fun onPermissionsDenied() { + if (!isVoiceOnlyCall) { + if (cameraEnumerator!!.deviceNames.size == 0) { + cameraControlButton!!.visibility = View.GONE + } else if (cameraEnumerator!!.deviceNames.size == 1) { + cameraSwitchButton!!.visibility = View.GONE + } + } + + if (activity != null && (EffortlessPermissions.hasPermissions( + activity, + *PERMISSIONS_CAMERA + ) || EffortlessPermissions.hasPermissions(activity, *PERMISSIONS_MICROPHONE)) + ) { + checkIfSomeAreApproved() + } else if (!isConnectionEstablished) { + fetchSignalingSettings() + } + } + + private fun onAudioManagerDevicesChanged( + device: MagicAudioManager.AudioDevice, + availableDevices: Set + ) { + Log.d( + TAG, "onAudioManagerDevicesChanged: " + availableDevices + ", " + + "selected: " + device + ) + + val shouldDisableProximityLock = (device == MagicAudioManager.AudioDevice.WIRED_HEADSET + || device == MagicAudioManager.AudioDevice.SPEAKER_PHONE + || device == MagicAudioManager.AudioDevice.BLUETOOTH) + + if (shouldDisableProximityLock) { + powerManagerUtils.updatePhoneState( + PowerManagerUtils.PhoneState.WITHOUT_PROXIMITY_SENSOR_LOCK + ) + } else { + powerManagerUtils.updatePhoneState(PowerManagerUtils.PhoneState.WITH_PROXIMITY_SENSOR_LOCK) + } + } + + private fun cameraInitialization() { + videoCapturer = createCameraCapturer(cameraEnumerator!!) + + //Create a VideoSource instance + if (videoCapturer != null) { + videoSource = peerConnectionFactory!!.createVideoSource(videoCapturer!!) + localVideoTrack = peerConnectionFactory!!.createVideoTrack("NCv0", videoSource!!) + localMediaStream!!.addTrack(localVideoTrack!!) + localVideoTrack!!.setEnabled(false) + localVideoTrack!!.addSink(pipVideoView) + } + } + + private fun microphoneInitialization() { + //create an AudioSource instance + audioSource = peerConnectionFactory!!.createAudioSource(audioConstraints) + localAudioTrack = peerConnectionFactory!!.createAudioTrack("NCa0", audioSource!!) + localAudioTrack!!.setEnabled(false) + localMediaStream!!.addTrack(localAudioTrack!!) + } + + private fun createCameraCapturer(enumerator: CameraEnumerator): VideoCapturer? { + val deviceNames = enumerator.deviceNames + + // First, try to find front facing camera + Logging.d(TAG, "Looking for front facing cameras.") + for (deviceName in deviceNames) { + if (enumerator.isFrontFacing(deviceName)) { + Logging.d(TAG, "Creating front facing camera capturer.") + val videoCapturer = enumerator.createCapturer(deviceName, null) + if (videoCapturer != null) { + pipVideoView!!.setMirror(true) + return videoCapturer + } + } + } + + // Front facing camera not found, try something else + Logging.d(TAG, "Looking for other cameras.") + for (deviceName in deviceNames) { + if (!enumerator.isFrontFacing(deviceName)) { + Logging.d(TAG, "Creating other camera capturer.") + val videoCapturer = enumerator.createCapturer(deviceName, null) + + if (videoCapturer != null) { + pipVideoView!!.setMirror(false) + return videoCapturer + } + } + } + + return null + } + + @OnLongClick(R.id.call_control_microphone) + internal fun onMicrophoneLongClick(): Boolean { + if (!audioOn) { + callControlHandler.removeCallbacksAndMessages(null) + cameraSwitchHandler.removeCallbacksAndMessages(null) + isPTTActive = true + callControls!!.visibility = View.VISIBLE + if (!isVoiceOnlyCall) { + cameraSwitchButton!!.visibility = View.VISIBLE + } + } + + onMicrophoneClick() + return true + } + + @OnClick(R.id.callControlEnableSpeaker) + fun onEnableSpeakerphoneClick() { + if (audioManager != null) { + audioManager!!.toggleUseSpeakerphone() + if (audioManager!!.isSpeakerphoneAutoOn) { + callControlEnableSpeaker!!.hierarchy + .setPlaceholderImage(R.drawable.ic_volume_up_white_24dp) + } else { + callControlEnableSpeaker!!.hierarchy + .setPlaceholderImage(R.drawable.ic_volume_mute_white_24dp) + } + } + } + + @OnClick(R.id.call_control_microphone) + fun onMicrophoneClick() { + if (activity != null && EffortlessPermissions.hasPermissions( + activity, + *PERMISSIONS_MICROPHONE + ) + ) { + + if (activity != null && !appPreferences!!.pushToTalkIntroShown) { + spotlightView = SpotlightView.Builder(activity!!) + .introAnimationDuration(300) + .enableRevealAnimation(true) + .performClick(false) + .fadeinTextDuration(400) + .headingTvColor(resources!!.getColor(R.color.colorPrimary)) + .headingTvSize(20) + .headingTvText(resources!!.getString(R.string.nc_push_to_talk)) + .subHeadingTvColor(resources!!.getColor(R.color.bg_default)) + .subHeadingTvSize(16) + .subHeadingTvText(resources!!.getString(R.string.nc_push_to_talk_desc)) + .maskColor(Color.parseColor("#dc000000")) + .target(microphoneControlButton) + .lineAnimDuration(400) + .lineAndArcColor(resources!!.getColor(R.color.colorPrimary)) + .enableDismissAfterShown(true) + .dismissOnBackPress(true) + .usageId("pushToTalk") + .show() + + appPreferences!!.pushToTalkIntroShown = true + } + + if (!isPTTActive) { + audioOn = !audioOn + + if (audioOn) { + microphoneControlButton!!.hierarchy.setPlaceholderImage(R.drawable.ic_mic_white_24px) + } else { + microphoneControlButton!!.hierarchy + .setPlaceholderImage(R.drawable.ic_mic_off_white_24px) + } + + toggleMedia(audioOn, false) + } else { + microphoneControlButton!!.hierarchy.setPlaceholderImage(R.drawable.ic_mic_white_24px) + pulseAnimation!!.start() + toggleMedia(true, false) + } + + if (isVoiceOnlyCall && !isConnectionEstablished) { + fetchSignalingSettings() + } + } else if (activity != null && EffortlessPermissions.somePermissionPermanentlyDenied( + activity!!, + *PERMISSIONS_MICROPHONE + ) + ) { + // Microphone permission is permanently denied so we cannot request it normally. + + OpenAppDetailsDialogFragment.show( + R.string.nc_microphone_permission_permanently_denied, + R.string.nc_permissions_settings, (activity as AppCompatActivity?)!! + ) + } else { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + requestPermissions(PERMISSIONS_MICROPHONE, 100) + } else { + onRequestPermissionsResult(100, PERMISSIONS_MICROPHONE, intArrayOf(1)) + } + } + } + + @OnClick(R.id.callControlHangupView) + internal fun onHangupClick() { + setCallState(CallStatus.LEAVING) + hangup(true) + } + + @OnClick(R.id.call_control_camera) + fun onCameraClick() { + if (activity != null && EffortlessPermissions.hasPermissions( + activity, + *PERMISSIONS_CAMERA + ) + ) { + videoOn = !videoOn + + if (videoOn) { + cameraControlButton!!.hierarchy.setPlaceholderImage(R.drawable.ic_videocam_white_24px) + if (cameraEnumerator!!.deviceNames.size > 1) { + cameraSwitchButton!!.visibility = View.VISIBLE + } + } else { + cameraControlButton!!.hierarchy + .setPlaceholderImage(R.drawable.ic_videocam_off_white_24px) + cameraSwitchButton!!.visibility = View.GONE + } + + toggleMedia(videoOn, true) + } else if (activity != null && EffortlessPermissions.somePermissionPermanentlyDenied( + activity!!, + *PERMISSIONS_CAMERA + ) + ) { + // Camera permission is permanently denied so we cannot request it normally. + OpenAppDetailsDialogFragment.show( + R.string.nc_camera_permission_permanently_denied, + R.string.nc_permissions_settings, (activity as AppCompatActivity?)!! + ) + } else { + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + requestPermissions(PERMISSIONS_CAMERA, 100) + } else { + onRequestPermissionsResult(100, PERMISSIONS_CAMERA, intArrayOf(1)) + } + } + } + + @OnClick(R.id.call_control_switch_camera, R.id.pip_video_view) + fun switchCamera() { + val cameraVideoCapturer = videoCapturer as CameraVideoCapturer? + cameraVideoCapturer?.switchCamera(object : CameraVideoCapturer.CameraSwitchHandler { + override fun onCameraSwitchDone(currentCameraIsFront: Boolean) { + pipVideoView!!.setMirror(currentCameraIsFront) + } + + override fun onCameraSwitchError(s: String) { + + } + }) + } + + private fun toggleMedia( + enable: Boolean, + video: Boolean + ) { + var message: String + if (video) { + message = "videoOff" + if (enable) { + cameraControlButton!!.alpha = 1.0f + message = "videoOn" + startVideoCapture() + } else { + cameraControlButton!!.alpha = 0.7f + if (videoCapturer != null) { + try { + videoCapturer!!.stopCapture() + } catch (e: InterruptedException) { + Log.d(TAG, "Failed to stop capturing video while sensor is near the ear") + } + + } + } + + if (localMediaStream != null && localMediaStream!!.videoTracks.size > 0) { + localMediaStream!!.videoTracks[0].setEnabled(enable) + } + if (enable) { + pipVideoView!!.visibility = View.VISIBLE + } else { + pipVideoView!!.visibility = View.INVISIBLE + } + } else { + message = "audioOff" + if (enable) { + message = "audioOn" + microphoneControlButton!!.alpha = 1.0f + } else { + microphoneControlButton!!.alpha = 0.7f + } + + if (localMediaStream != null && localMediaStream!!.audioTracks.size > 0) { + localMediaStream!!.audioTracks[0].setEnabled(enable) + } + } + + if (isConnectionEstablished) { + if (!hasMCU) { + for (i in magicPeerConnectionWrapperList.indices) { + magicPeerConnectionWrapperList[i].sendChannelData(DataChannelMessage(message)) + } + } else { + for (i in magicPeerConnectionWrapperList.indices) { + if (magicPeerConnectionWrapperList[i] + .sessionId == webSocketClient!!.sessionId + ) { + magicPeerConnectionWrapperList[i].sendChannelData(DataChannelMessage(message)) + break + } + } + } + } + } + + private fun animateCallControls( + show: Boolean, + startDelay: Long + ) { + if (isVoiceOnlyCall) { + if (spotlightView != null && spotlightView!!.visibility != View.GONE) { + spotlightView!!.visibility = View.GONE + } + } else if (!isPTTActive) { + val alpha: Float + val duration: Long + + if (show) { + callControlHandler.removeCallbacksAndMessages(null) + cameraSwitchHandler.removeCallbacksAndMessages(null) + alpha = 1.0f + duration = 1000 + if (callControls!!.visibility != View.VISIBLE) { + callControls!!.alpha = 0.0f + callControls!!.visibility = View.VISIBLE + + cameraSwitchButton!!.alpha = 0.0f + cameraSwitchButton!!.visibility = View.VISIBLE + } else { + callControlHandler.postDelayed({ animateCallControls(false, 0) }, 5000) + return + } + } else { + alpha = 0.0f + duration = 1000 + } + + if (callControls != null) { + callControls!!.isEnabled = false + callControls!!.animate() + .translationY(0f) + .alpha(alpha) + .setDuration(duration) + .setStartDelay(startDelay) + .setListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + super.onAnimationEnd(animation) + if (callControls != null) { + if (!show) { + callControls!!.visibility = View.GONE + if (spotlightView != null && spotlightView!!.visibility != View.GONE) { + spotlightView!!.visibility = View.GONE + } + } else { + callControlHandler.postDelayed({ + if (!isPTTActive) { + animateCallControls(false, 0) + } + }, 7500) + } + + callControls!!.isEnabled = true + } + } + }) + } + + if (cameraSwitchButton != null) { + cameraSwitchButton!!.isEnabled = false + cameraSwitchButton!!.animate() + .translationY(0f) + .alpha(alpha) + .setDuration(duration) + .setStartDelay(startDelay) + .setListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + super.onAnimationEnd(animation) + if (cameraSwitchButton != null) { + if (!show) { + cameraSwitchButton!!.visibility = View.GONE + } + + cameraSwitchButton!!.isEnabled = true + } + } + }) + } + } + } + + public override fun onDestroy() { + if (currentCallStatus != CallStatus.LEAVING) { + onHangupClick() + } + powerManagerUtils.updatePhoneState(PowerManagerUtils.PhoneState.IDLE) + super.onDestroy() + } + + private fun fetchSignalingSettings() { + ncApi!!.getSignalingSettings(credentials, ApiUtils.getUrlForSignalingSettings(baseUrl)) + .subscribeOn(Schedulers.io()) + .retry(3) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + + } + + override fun onNext(signalingSettingsOverall: SignalingSettingsOverall) { + var iceServer: IceServer + if (signalingSettingsOverall.ocs != null && + signalingSettingsOverall.ocs.settings != null + ) { + + externalSignalingServer = ExternalSignalingServer() + + if (!TextUtils.isEmpty( + signalingSettingsOverall.ocs.settings.externalSignalingServer + ) && !TextUtils.isEmpty( + signalingSettingsOverall.ocs + .settings + .externalSignalingTicket + ) + ) { + externalSignalingServer = ExternalSignalingServer() + externalSignalingServer!!.externalSignalingServer = signalingSettingsOverall.ocs.settings.externalSignalingServer + externalSignalingServer!!.externalSignalingTicket = signalingSettingsOverall.ocs.settings.externalSignalingTicket + hasExternalSignalingServer = true + } else { + hasExternalSignalingServer = false + } + + if (conversationUser!!.userId != "?") { + try { + userUtils!!.createOrUpdateUser( + null, null, null, null, null, null, null, + conversationUser.id, null, null, + LoganSquare.serialize(externalSignalingServer!!) + ) + .subscribeOn(Schedulers.io()) + .subscribe() + } catch (exception: IOException) { + Log.e(TAG, "Failed to serialize external signaling server") + } + + } + + if (signalingSettingsOverall.ocs.settings.stunServers != null) { + for (i in 0 until signalingSettingsOverall.ocs.settings.stunServers.size) { + iceServer = signalingSettingsOverall.ocs.settings.stunServers[i] + if (TextUtils.isEmpty(iceServer.username) || TextUtils.isEmpty( + iceServer + .credential + ) + ) { + iceServers!!.add(PeerConnection.IceServer(iceServer.url)) + } else { + iceServers!!.add( + PeerConnection.IceServer( + iceServer.url, + iceServer.username, iceServer.credential + ) + ) + } + } + } + + if (signalingSettingsOverall.ocs.settings.turnServers != null) { + for (i in 0 until signalingSettingsOverall.ocs.settings.turnServers.size) { + iceServer = signalingSettingsOverall.ocs.settings.turnServers[i] + for (j in 0 until iceServer.urls.size) { + if (TextUtils.isEmpty(iceServer.username) || TextUtils.isEmpty( + iceServer + .credential + ) + ) { + iceServers!!.add(PeerConnection.IceServer(iceServer.urls[j])) + } else { + iceServers!!.add( + PeerConnection.IceServer( + iceServer.urls[j], + iceServer.username, iceServer.credential + ) + ) + } + } + } + } + } + + checkCapabilities() + } + + override fun onError(e: Throwable) {} + + override fun onComplete() { + + } + }) + } + + private fun checkCapabilities() { + ncApi!!.getCapabilities(credentials, ApiUtils.getUrlForCapabilities(baseUrl)) + .retry(3) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + + } + + override fun onNext(capabilitiesOverall: CapabilitiesOverall) { + isMultiSession = capabilitiesOverall.ocs.data + .capabilities != null && capabilitiesOverall.ocs.data + .capabilities.spreedCapability != null && + capabilitiesOverall.ocs.data + .capabilities.spreedCapability + .features != null && capabilitiesOverall.ocs.data + .capabilities.spreedCapability + .features.contains("multi-room-users") + + needsPing = !(capabilitiesOverall.ocs.data + .capabilities != null && capabilitiesOverall.ocs.data + .capabilities.spreedCapability != null && + capabilitiesOverall.ocs.data + .capabilities.spreedCapability + .features != null && capabilitiesOverall.ocs.data + .capabilities.spreedCapability + .features.contains("no-ping")) + + if (!hasExternalSignalingServer) { + joinRoomAndCall() + } else { + setupAndInitiateWebSocketsConnection() + } + } + + override fun onError(e: Throwable) { + isMultiSession = false + } + + override fun onComplete() { + + } + }) + } + + private fun joinRoomAndCall() { + ncApi!!.joinRoom( + credentials, ApiUtils.getUrlForSettingMyselfAsActiveParticipant( + baseUrl, + roomToken + ), conversationPassword + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .retry(3) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + + } + + override fun onNext(roomOverall: RoomOverall) { + callSession = roomOverall.ocs.data + .sessionId + callOrJoinRoomViaWebSocket() + } + + override fun onError(e: Throwable) { + + } + + override fun onComplete() { + + } + }) + } + + private fun callOrJoinRoomViaWebSocket() { + if (!hasExternalSignalingServer) { + performCall() + } else { + webSocketClient!!.joinRoomWithRoomTokenAndSession(roomToken, callSession) + } + } + + private fun performCall() { + ncApi!!.joinCall( + credentials, + ApiUtils.getUrlForCall(baseUrl, roomToken) + ) + .subscribeOn(Schedulers.io()) + .retry(3) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + + } + + override fun onNext(genericOverall: GenericOverall) { + if (currentCallStatus != CallStatus.LEAVING) { + setCallState(CallStatus.ESTABLISHED) + + if (needsPing) { + ncApi!!.pingCall(credentials, ApiUtils.getUrlForCallPing(baseUrl, roomToken)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .repeatWhen { observable -> observable.delay(5000, TimeUnit.MILLISECONDS) } + .takeWhile { observable -> isConnectionEstablished } + .retry(3) { observable -> isConnectionEstablished } + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + pingDisposable = d + } + + override fun onNext(genericOverall: GenericOverall) { + + } + + override fun onError(e: Throwable) { + dispose(pingDisposable) + } + + override fun onComplete() { + dispose(pingDisposable) + } + }) + } + + // Start pulling signaling messages + var urlToken: String? = null + if (isMultiSession) { + urlToken = roomToken + } + + if (!conversationUser!!.hasSpreedFeatureCapability("no-ping") && !TextUtils.isEmpty( + roomId + ) + ) { + NotificationUtils.cancelExistingNotificationsForRoom( + applicationContext, conversationUser, roomId + ) + } else if (!TextUtils.isEmpty(roomToken)) { + NotificationUtils.cancelExistingNotificationsForRoom( + applicationContext, conversationUser, roomToken!! + ) + } + + if (!hasExternalSignalingServer) { + ncApi!!.pullSignalingMessages( + credentials, + ApiUtils.getUrlForSignaling(baseUrl, urlToken) + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .repeatWhen { observable -> observable } + .takeWhile { observable -> isConnectionEstablished } + .retry(3) { observable -> isConnectionEstablished } + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + signalingDisposable = d + } + + override fun onNext(signalingOverall: SignalingOverall) { + if (signalingOverall.ocs.signalings != null) { + for (i in 0 until signalingOverall.ocs.signalings.size) { + try { + receivedSignalingMessage( + signalingOverall.ocs.signalings[i] + ) + } catch (e: IOException) { + Log.e(TAG, "Failed to process received signaling" + " message") + } + + } + } + } + + override fun onError(e: Throwable) { + dispose(signalingDisposable) + } + + override fun onComplete() { + dispose(signalingDisposable) + } + }) + } + } + } + + override fun onError(e: Throwable) {} + + override fun onComplete() { + + } + }) + } + + private fun setupAndInitiateWebSocketsConnection() { + if (webSocketConnectionHelper == null) { + webSocketConnectionHelper = WebSocketConnectionHelper() + } + + if (webSocketClient == null) { + webSocketClient = WebSocketConnectionHelper.getExternalSignalingInstanceForServer( + externalSignalingServer!!.externalSignalingServer, + conversationUser, externalSignalingServer!!.externalSignalingTicket, + TextUtils.isEmpty(credentials) + ) + } else { + if (webSocketClient!!.isConnected && currentCallStatus == CallStatus.PUBLISHER_FAILED) { + webSocketClient!!.restartWebSocket() + } + } + + joinRoomAndCall() + } + + private fun initiateCall() { + if (!TextUtils.isEmpty(roomToken)) { + checkPermissions() + } else { + handleFromNotification() + } + } + + override fun onDetach(view: View) { + eventBus!!.unregister(this) + super.onDetach(view) + } + + override fun onAttach(view: View) { + super.onAttach(view) + eventBus!!.register(this) + } + + @Subscribe(threadMode = ThreadMode.BACKGROUND) + fun onMessageEvent(webSocketCommunicationEvent: WebSocketCommunicationEvent) { + when (webSocketCommunicationEvent.type) { + "hello" -> if (!webSocketCommunicationEvent.hashMap!!.containsKey("oldResumeId")) { + if (currentCallStatus == CallStatus.RECONNECTING) { + hangup(false) + } else { + initiateCall() + } + } else { + } + "roomJoined" -> { + startSendingNick() + + if (webSocketCommunicationEvent.hashMap!!["roomToken"] == roomToken) { + performCall() + } + } + "participantsUpdate" -> if (webSocketCommunicationEvent.hashMap!!["roomToken"] == roomToken) { + processUsersInRoom( + webSocketClient!!.getJobWithId( + Integer.valueOf(webSocketCommunicationEvent.hashMap!!["jobId"]!!) + ) as List> + ) + } + "signalingMessage" -> processMessage( + webSocketClient!!.getJobWithId( + Integer.valueOf(webSocketCommunicationEvent.hashMap!!["jobId"]!!) + ) as NCSignalingMessage + ) + "peerReadyForRequestingOffer" -> webSocketClient!!.requestOfferForSessionIdWithType( + webSocketCommunicationEvent.hashMap!!["sessionId"], "video" + ) + } + } + + @OnClick(R.id.pip_video_view, R.id.remote_renderers_layout) + fun showCallControls() { + animateCallControls(true, 0) + } + + private fun dispose(disposable: Disposable?) { + if (disposable != null && !disposable.isDisposed) { + disposable.dispose() + } else if (disposable == null) { + + if (pingDisposable != null && !pingDisposable!!.isDisposed) { + pingDisposable!!.dispose() + pingDisposable = null + } + + if (signalingDisposable != null && !signalingDisposable!!.isDisposed) { + signalingDisposable!!.dispose() + signalingDisposable = null + } + } + } + + @Throws(IOException::class) + private fun receivedSignalingMessage(signaling: Signaling) { + val messageType = signaling.type + + if (!isConnectionEstablished && currentCallStatus != CallStatus.CALLING) { + return + } + + if ("usersInRoom" == messageType) { + processUsersInRoom(signaling.messageWrapper as List>) + } else if ("message" == messageType) { + val ncSignalingMessage = LoganSquare.parse( + signaling.messageWrapper.toString(), + NCSignalingMessage::class.java + ) + processMessage(ncSignalingMessage) + } else { + Log.d(TAG, "Something went very very wrong") + } + } + + private fun processMessage(ncSignalingMessage: NCSignalingMessage) { + if (ncSignalingMessage.roomType == "video" || ncSignalingMessage.roomType == "screen") { + val magicPeerConnectionWrapper = getPeerConnectionWrapperForSessionIdAndType( + ncSignalingMessage.from, + ncSignalingMessage.roomType, false + ) + + var type: String? = null + if (ncSignalingMessage.payload != null && ncSignalingMessage.payload.type != null) { + type = ncSignalingMessage.payload.type + } else if (ncSignalingMessage.type != null) { + type = ncSignalingMessage.type + } + + if (type != null) { + when (type) { + "unshareScreen" -> endPeerConnection(ncSignalingMessage.from, true) + "offer", "answer" -> { + magicPeerConnectionWrapper.nick = ncSignalingMessage.payload.nick + val sessionDescriptionWithPreferredCodec: SessionDescription + + val sessionDescriptionStringWithPreferredCodec = MagicWebRTCUtils.preferCodec( + ncSignalingMessage.payload.sdp, + "H264", false + ) + + sessionDescriptionWithPreferredCodec = SessionDescription( + SessionDescription.Type.fromCanonicalForm(type), + sessionDescriptionStringWithPreferredCodec + ) + + if (magicPeerConnectionWrapper.peerConnection != null) { + magicPeerConnectionWrapper.peerConnection + .setRemoteDescription( + magicPeerConnectionWrapper + .magicSdpObserver, sessionDescriptionWithPreferredCodec + ) + } + } + "candidate" -> { + val ncIceCandidate = ncSignalingMessage.payload.iceCandidate + val iceCandidate = IceCandidate( + ncIceCandidate.sdpMid, + ncIceCandidate.sdpMLineIndex, ncIceCandidate.candidate + ) + magicPeerConnectionWrapper.addCandidate(iceCandidate) + } + "endOfCandidates" -> magicPeerConnectionWrapper.drainIceCandidates() + else -> { + } + } + } + } else { + Log.d(TAG, "Something went very very wrong") + } + } + + private fun hangup(shutDownView: Boolean) { + stopCallingSound() + dispose(null) + + if (shutDownView) { + + if (videoCapturer != null) { + try { + videoCapturer!!.stopCapture() + } catch (e: InterruptedException) { + Log.e(TAG, "Failed to stop capturing while hanging up") + } + + videoCapturer!!.dispose() + videoCapturer = null + } + + if (pipVideoView != null) { + pipVideoView!!.release() + } + + if (audioSource != null) { + audioSource!!.dispose() + audioSource = null + } + + if (audioManager != null) { + audioManager!!.stop() + audioManager = null + } + + if (videoSource != null) { + videoSource = null + } + + if (peerConnectionFactory != null) { + peerConnectionFactory = null + } + + localMediaStream = null + localAudioTrack = null + localVideoTrack = null + + if (TextUtils.isEmpty(credentials) && hasExternalSignalingServer) { + WebSocketConnectionHelper.deleteExternalSignalingInstanceForUserEntity(-1) + } + } + + for (i in magicPeerConnectionWrapperList.indices) { + endPeerConnection(magicPeerConnectionWrapperList[i].sessionId, false) + } + + hangupNetworkCalls(shutDownView) + } + + private fun hangupNetworkCalls(shutDownView: Boolean) { + ncApi!!.leaveCall(credentials, ApiUtils.getUrlForCall(baseUrl, roomToken)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + + } + + override fun onNext(genericOverall: GenericOverall) { + if (!TextUtils.isEmpty(credentials) && hasExternalSignalingServer) { + webSocketClient!!.joinRoomWithRoomTokenAndSession("", callSession) + } + + if (isMultiSession) { + if (shutDownView && activity != null) { + activity!!.finish() + } else if (!shutDownView && (currentCallStatus == CallStatus.RECONNECTING || currentCallStatus == CallStatus.PUBLISHER_FAILED)) { + initiateCall() + } + } else { + leaveRoom(shutDownView) + } + } + + override fun onError(e: Throwable) { + + } + + override fun onComplete() { + + } + }) + } + + private fun leaveRoom(shutDownView: Boolean) { + ncApi!!.leaveRoom( + credentials, + ApiUtils.getUrlForSettingMyselfAsActiveParticipant(baseUrl, roomToken) + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + + } + + override fun onNext(genericOverall: GenericOverall) { + if (shutDownView && activity != null) { + activity!!.finish() + } + } + + override fun onError(e: Throwable) { + + } + + override fun onComplete() { + + } + }) + } + + private fun startVideoCapture() { + if (videoCapturer != null) { + videoCapturer!!.startCapture(1280, 720, 30) + } + } + + private fun processUsersInRoom(users: List>) { + val newSessions = ArrayList() + val oldSesssions = HashSet() + + for (participant in users) { + if (participant["sessionId"] != callSession) { + val inCallObject = participant["inCall"] + val isNewSession: Boolean + if (inCallObject is Boolean) { + isNewSession = inCallObject + } else { + isNewSession = inCallObject as Long != 0L + } + + if (isNewSession) { + newSessions.add(participant["sessionId"]!!.toString()) + } else { + oldSesssions.add(participant["sessionId"]!!.toString()) + } + } + } + + for (magicPeerConnectionWrapper in magicPeerConnectionWrapperList) { + if (!magicPeerConnectionWrapper.isMCUPublisher) { + oldSesssions.add(magicPeerConnectionWrapper.sessionId) + } + } + + // Calculate sessions that left the call + oldSesssions.removeAll(newSessions) + + // Calculate sessions that join the call + newSessions.removeAll(oldSesssions) + + if (!isConnectionEstablished && currentCallStatus != CallStatus.CALLING) { + return + } + + if (newSessions.size > 0 && !hasMCU) { + getPeersForCall() + } + + hasMCU = hasExternalSignalingServer && webSocketClient != null && webSocketClient!!.hasMCU() + + for (sessionId in newSessions) { + getPeerConnectionWrapperForSessionIdAndType( + sessionId, "video", + hasMCU && sessionId == webSocketClient!!.sessionId + ) + } + + if (newSessions.size > 0 && currentCallStatus != CallStatus.IN_CONVERSATION) { + setCallState(CallStatus.IN_CONVERSATION) + } + + for (sessionId in oldSesssions) { + endPeerConnection(sessionId, false) + } + } + + private fun getPeersForCall() { + ncApi!!.getPeersForCall(credentials, ApiUtils.getUrlForCall(baseUrl, roomToken)) + .subscribeOn(Schedulers.io()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + + } + + override fun onNext(participantsOverall: ParticipantsOverall) { + participantMap = HashMap() + for (participant in participantsOverall.ocs.data) { + participantMap[participant.sessionId] = participant + if (activity != null) { + activity!!.runOnUiThread { setupAvatarForSession(participant.sessionId) } + } + } + } + + override fun onError(e: Throwable) { + + } + + override fun onComplete() { + + } + }) + } + + private fun deleteMagicPeerConnection(magicPeerConnectionWrapper: MagicPeerConnectionWrapper) { + magicPeerConnectionWrapper.removePeerConnection() + magicPeerConnectionWrapperList.remove(magicPeerConnectionWrapper) + } + + private fun getPeerConnectionWrapperForSessionId( + sessionId: String, + type: String + ): MagicPeerConnectionWrapper? { + for (i in magicPeerConnectionWrapperList.indices) { + if (magicPeerConnectionWrapperList[i].sessionId == sessionId && magicPeerConnectionWrapperList[i].videoStreamType == type) { + return magicPeerConnectionWrapperList[i] + } + } + + return null + } + + private fun getPeerConnectionWrapperForSessionIdAndType( + sessionId: String, + type: String, + publisher: Boolean + ): MagicPeerConnectionWrapper { + var magicPeerConnectionWrapper: MagicPeerConnectionWrapper? = getPeerConnectionWrapperForSessionId(sessionId, type) + if (magicPeerConnectionWrapper != null) { + return magicPeerConnectionWrapper + } else { + if (hasMCU && publisher) { + magicPeerConnectionWrapper = MagicPeerConnectionWrapper( + peerConnectionFactory!!, + iceServers, sdpConstraintsForMCU, sessionId, callSession, localMediaStream, true, true, + type + ) + } else if (hasMCU) { + magicPeerConnectionWrapper = MagicPeerConnectionWrapper( + peerConnectionFactory!!, + iceServers, sdpConstraints, sessionId, callSession, null, false, true, type + ) + } else { + if ("screen" != type) { + magicPeerConnectionWrapper = MagicPeerConnectionWrapper( + peerConnectionFactory!!, + iceServers, sdpConstraints, sessionId, callSession, localMediaStream, false, false, + type + ) + } else { + magicPeerConnectionWrapper = MagicPeerConnectionWrapper( + peerConnectionFactory!!, + iceServers, sdpConstraints, sessionId, callSession, null, false, false, type + ) + } + } + + magicPeerConnectionWrapperList.add(magicPeerConnectionWrapper) + + if (publisher) { + startSendingNick() + } + + return magicPeerConnectionWrapper + } + } + + private fun getPeerConnectionWrapperListForSessionId( + sessionId: String + ): List { + val internalList = ArrayList() + for (magicPeerConnectionWrapper in magicPeerConnectionWrapperList) { + if (magicPeerConnectionWrapper.sessionId == sessionId) { + internalList.add(magicPeerConnectionWrapper) + } + } + + return internalList + } + + private fun endPeerConnection( + sessionId: String, + justScreen: Boolean + ) { + val magicPeerConnectionWrappers: List = getPeerConnectionWrapperListForSessionId(sessionId) + var magicPeerConnectionWrapper: MagicPeerConnectionWrapper + if (!magicPeerConnectionWrappers.isEmpty() && activity != null + ) { + for (i in magicPeerConnectionWrappers.indices) { + magicPeerConnectionWrapper = magicPeerConnectionWrappers[i] + if (magicPeerConnectionWrapper.sessionId == sessionId) { + if (magicPeerConnectionWrapper.videoStreamType == "screen" || !justScreen) { + activity!!.runOnUiThread { + removeMediaStream( + sessionId + "+" + + magicPeerConnectionWrapper.videoStreamType + ) + } + deleteMagicPeerConnection(magicPeerConnectionWrapper) + } + } + } + } + } + + private fun removeMediaStream(sessionId: String) { + if (remoteRenderersLayout != null && remoteRenderersLayout!!.childCount > 0) { + val relativeLayout = remoteRenderersLayout!!.findViewWithTag(sessionId) + if (relativeLayout != null) { + val surfaceViewRenderer = + relativeLayout.findViewById(R.id.surface_view) + surfaceViewRenderer.release() + remoteRenderersLayout!!.removeView(relativeLayout) + remoteRenderersLayout!!.invalidate() + } + } + + if (callControls != null) { + callControls!!.z = 100.0f + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onMessageEvent(configurationChangeEvent: ConfigurationChangeEvent) { + powerManagerUtils.setOrientation(resources!!.configuration.orientation) + + if (resources!!.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { + remoteRenderersLayout!!.orientation = LinearLayout.HORIZONTAL + } else if (resources!!.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) { + remoteRenderersLayout!!.orientation = LinearLayout.VERTICAL + } + + setPipVideoViewDimensions() + + cookieManager!!.cookieStore.removeAll() + } + + private fun setPipVideoViewDimensions() { + val layoutParams = pipVideoView!!.layoutParams as FrameLayout.LayoutParams + + if (resources!!.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { + remoteRenderersLayout!!.orientation = LinearLayout.HORIZONTAL + layoutParams.height = resources!!.getDimension(R.dimen.large_preview_dimension) + .toInt() + layoutParams.width = FrameLayout.LayoutParams.WRAP_CONTENT + pipVideoView!!.layoutParams = layoutParams + } else if (resources!!.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) { + remoteRenderersLayout!!.orientation = LinearLayout.VERTICAL + layoutParams.height = FrameLayout.LayoutParams.WRAP_CONTENT + layoutParams.width = resources!!.getDimension(R.dimen.large_preview_dimension) + .toInt() + pipVideoView!!.layoutParams = layoutParams + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onMessageEvent(peerConnectionEvent: PeerConnectionEvent) { + if (peerConnectionEvent.peerConnectionEventType == PeerConnectionEvent.PeerConnectionEventType + .PEER_CLOSED + ) { + endPeerConnection( + peerConnectionEvent.sessionId, + peerConnectionEvent.videoStreamType == "screen" + ) + } else if (peerConnectionEvent.peerConnectionEventType == PeerConnectionEvent + .PeerConnectionEventType.SENSOR_FAR || peerConnectionEvent.peerConnectionEventType == PeerConnectionEvent + .PeerConnectionEventType.SENSOR_NEAR + ) { + + if (!isVoiceOnlyCall) { + val enableVideo = peerConnectionEvent.peerConnectionEventType == PeerConnectionEvent + .PeerConnectionEventType.SENSOR_FAR && videoOn + if (activity != null && EffortlessPermissions.hasPermissions( + activity, + *PERMISSIONS_CAMERA + ) && + (currentCallStatus == CallStatus.CALLING || isConnectionEstablished) && videoOn + && enableVideo != localVideoTrack!!.enabled() + ) { + toggleMedia(enableVideo, true) + } + } + } else if (peerConnectionEvent.peerConnectionEventType == PeerConnectionEvent + .PeerConnectionEventType.NICK_CHANGE + ) { + gotNick( + peerConnectionEvent.sessionId, peerConnectionEvent.nick, true, + peerConnectionEvent.videoStreamType + ) + } else if (peerConnectionEvent.peerConnectionEventType == PeerConnectionEvent + .PeerConnectionEventType.VIDEO_CHANGE && !isVoiceOnlyCall + ) { + gotAudioOrVideoChange( + true, + peerConnectionEvent.sessionId + "+" + peerConnectionEvent.videoStreamType, + peerConnectionEvent.changeValue!! + ) + } else if (peerConnectionEvent.peerConnectionEventType == PeerConnectionEvent + .PeerConnectionEventType.AUDIO_CHANGE + ) { + gotAudioOrVideoChange( + false, + peerConnectionEvent.sessionId + "+" + peerConnectionEvent.videoStreamType, + peerConnectionEvent.changeValue!! + ) + } else if (peerConnectionEvent.peerConnectionEventType == PeerConnectionEvent.PeerConnectionEventType.PUBLISHER_FAILED) { + currentCallStatus = CallStatus.PUBLISHER_FAILED + webSocketClient!!.clearResumeId() + hangup(false) + } + } + + private fun startSendingNick() { + val dataChannelMessage = DataChannelMessageNick() + dataChannelMessage.type = "nickChanged" + val nickChangedPayload = HashMap() + nickChangedPayload["userid"] = conversationUser!!.userId + nickChangedPayload["name"] = conversationUser.displayName.toString() + dataChannelMessage.payload = nickChangedPayload + val magicPeerConnectionWrapper: MagicPeerConnectionWrapper + for (i in magicPeerConnectionWrapperList.indices) { + if (magicPeerConnectionWrapperList[i].isMCUPublisher) { + magicPeerConnectionWrapper = magicPeerConnectionWrapperList[i] + Observable + .interval(1, TimeUnit.SECONDS) + .repeatUntil { !isConnectionEstablished || isBeingDestroyed || isDestroyed } + .observeOn(Schedulers.io()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + + } + + override fun onNext(aLong: Long) { + magicPeerConnectionWrapper.sendNickChannelData(dataChannelMessage) + } + + override fun onError(e: Throwable) { + + } + + override fun onComplete() { + + } + }) + break + } + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onMessageEvent(mediaStreamEvent: MediaStreamEvent) { + if (mediaStreamEvent.mediaStream != null) { + setupVideoStreamForLayout( + mediaStreamEvent.mediaStream, mediaStreamEvent.session, + mediaStreamEvent.mediaStream.videoTracks != null && mediaStreamEvent.mediaStream.videoTracks.size > 0, + mediaStreamEvent.videoStreamType + ) + } else { + setupVideoStreamForLayout( + null, mediaStreamEvent.session, false, + mediaStreamEvent.videoStreamType + ) + } + } + + @Subscribe(threadMode = ThreadMode.BACKGROUND) + @Throws(IOException::class) + fun onMessageEvent(sessionDescriptionSend: SessionDescriptionSendEvent) { + val ncMessageWrapper = NCMessageWrapper() + ncMessageWrapper.ev = "message" + ncMessageWrapper.sessionId = callSession + val ncSignalingMessage = NCSignalingMessage() + ncSignalingMessage.to = sessionDescriptionSend.peerId + ncSignalingMessage.roomType = sessionDescriptionSend.videoStreamType + ncSignalingMessage.type = sessionDescriptionSend.type + val ncMessagePayload = NCMessagePayload() + ncMessagePayload.type = sessionDescriptionSend.type + + if ("candidate" != sessionDescriptionSend.type) { + ncMessagePayload.sdp = sessionDescriptionSend.sessionDescription!!.description + ncMessagePayload.nick = conversationUser!!.displayName + } else { + ncMessagePayload.iceCandidate = sessionDescriptionSend.ncIceCandidate + } + + // Set all we need + ncSignalingMessage.payload = ncMessagePayload + ncMessageWrapper.signalingMessage = ncSignalingMessage + + if (!hasExternalSignalingServer) { + val stringBuilder = StringBuilder() + stringBuilder.append("{") + .append("\"fn\":\"") + .append( + StringEscapeUtils.escapeJson( + LoganSquare.serialize(ncMessageWrapper.signalingMessage) + ) + ) + .append("\"") + .append(",") + .append("\"sessionId\":") + .append("\"") + .append(StringEscapeUtils.escapeJson(callSession)) + .append("\"") + .append(",") + .append("\"ev\":\"message\"") + .append("}") + + val strings = ArrayList() + val stringToSend = stringBuilder.toString() + strings.add(stringToSend) + + var urlToken: String? = null + if (isMultiSession) { + urlToken = roomToken + } + + ncApi!!.sendSignalingMessages( + credentials, ApiUtils.getUrlForSignaling(baseUrl, urlToken), + strings.toString() + ) + .retry(3) + .subscribeOn(Schedulers.io()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + + } + + override fun onNext(signalingOverall: SignalingOverall) { + if (signalingOverall.ocs.signalings != null) { + for (i in 0 until signalingOverall.ocs.signalings.size) { + try { + receivedSignalingMessage(signalingOverall.ocs.signalings[i]) + } catch (e: IOException) { + e.printStackTrace() + } + + } + } + } + + override fun onError(e: Throwable) {} + + override fun onComplete() { + + } + }) + } else { + webSocketClient!!.sendCallMessage(ncMessageWrapper) + } + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + + EffortlessPermissions.onRequestPermissionsResult( + requestCode, permissions, grantResults, + this + ) + } + + private fun setupAvatarForSession(session: String) { + if (remoteRenderersLayout != null) { + val relativeLayout = remoteRenderersLayout!!.findViewWithTag("$session+video") + if (relativeLayout != null) { + val avatarImageView = relativeLayout.findViewById(R.id.avatarImageView) + + val userId: String + + if (hasMCU) { + userId = webSocketClient!!.getUserIdForSession(session) + } else { + userId = participantMap[session]!!.userId + } + + if (!TextUtils.isEmpty(userId)) { + + if (activity != null) { + avatarImageView.controller = null + + val draweeController = Fresco.newDraweeControllerBuilder() + .setOldController(avatarImageView.controller) + .setImageRequest( + DisplayUtils.getImageRequestForUrl( + ApiUtils.getUrlForAvatarWithName( + baseUrl, + userId, + R.dimen.avatar_size_big + ), null + ) + ) + .build() + avatarImageView.controller = draweeController + } + } + } + } + } + + private fun setupVideoStreamForLayout( + mediaStream: MediaStream?, + session: String, + enable: Boolean, + videoStreamType: String + ) { + var isInitialLayoutSetupForPeer = false + if (remoteRenderersLayout!!.findViewWithTag(session) == null) { + setupNewPeerLayout(session, videoStreamType) + isInitialLayoutSetupForPeer = true + } + + val relativeLayout = + remoteRenderersLayout!!.findViewWithTag("$session+$videoStreamType") + val surfaceViewRenderer = relativeLayout.findViewById(R.id.surface_view) + val imageView = relativeLayout.findViewById(R.id.avatarImageView) + + if (mediaStream != null + && mediaStream.videoTracks != null + && mediaStream.videoTracks.size > 0 + && enable + ) { + val videoTrack = mediaStream.videoTracks[0] + + videoTrack.addSink(surfaceViewRenderer) + + imageView.visibility = View.INVISIBLE + surfaceViewRenderer.visibility = View.VISIBLE + } else { + imageView.visibility = View.VISIBLE + surfaceViewRenderer.visibility = View.INVISIBLE + + if (isInitialLayoutSetupForPeer && isVoiceOnlyCall) { + gotAudioOrVideoChange(true, session, false) + } + } + + callControls!!.z = 100.0f + } + + private fun gotAudioOrVideoChange( + video: Boolean, + sessionId: String, + change: Boolean + ) { + val relativeLayout = remoteRenderersLayout!!.findViewWithTag(sessionId) + if (relativeLayout != null) { + val imageView: ImageView + val avatarImageView = relativeLayout.findViewById(R.id.avatarImageView) + val surfaceViewRenderer = relativeLayout.findViewById(R.id.surface_view) + + if (video) { + imageView = relativeLayout.findViewById(R.id.remote_video_off) + + if (change) { + avatarImageView.visibility = View.INVISIBLE + surfaceViewRenderer.visibility = View.VISIBLE + } else { + avatarImageView.visibility = View.VISIBLE + surfaceViewRenderer.visibility = View.INVISIBLE + } + } else { + imageView = relativeLayout.findViewById(R.id.remote_audio_off) + } + + if (change && imageView.visibility != View.INVISIBLE) { + imageView.visibility = View.INVISIBLE + } else if (!change && imageView.visibility != View.VISIBLE) { + imageView.visibility = View.VISIBLE + } + } + } + + private fun setupNewPeerLayout( + session: String, + type: String + ) { + if (remoteRenderersLayout!!.findViewWithTag( + "$session+$type" + ) == null && activity != null + ) { + activity!!.runOnUiThread { + val relativeLayout = activity!!.layoutInflater.inflate( + R.layout.call_item, remoteRenderersLayout, + false + ) as RelativeLayout + relativeLayout.tag = "$session+$type" + val surfaceViewRenderer = relativeLayout.findViewById( + R.id + .surface_view + ) + + surfaceViewRenderer.setMirror(false) + surfaceViewRenderer.init(rootEglBase!!.eglBaseContext, null) + surfaceViewRenderer.setZOrderMediaOverlay(false) + // disabled because it causes some devices to crash + surfaceViewRenderer.setEnableHardwareScaler(false) + surfaceViewRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT) + surfaceViewRenderer.setOnClickListener(videoOnClickListener) + remoteRenderersLayout!!.addView(relativeLayout) + if (hasExternalSignalingServer) { + gotNick(session, webSocketClient!!.getDisplayNameForSession(session), false, type) + } else { + gotNick( + session, + getPeerConnectionWrapperForSessionIdAndType(session, type, false).nick, false, + type + ) + } + + if ("video" == type) { + setupAvatarForSession(session) + } + + callControls!!.z = 100.0f + } + } + } + + private fun gotNick( + sessionOrUserId: String, + nick: String, + isFromAnEvent: Boolean, + type: String + ) { + var sessionOrUserId = sessionOrUserId + if (isFromAnEvent && hasExternalSignalingServer) { + // get session based on userId + sessionOrUserId = webSocketClient!!.getSessionForUserId(sessionOrUserId) + } + + sessionOrUserId += "+$type" + + if (relativeLayout != null) { + val relativeLayout = remoteRenderersLayout!!.findViewWithTag(sessionOrUserId) + val textView = relativeLayout.findViewById(R.id.peer_nick_text_view) + if (textView.text != nick) { + textView.text = nick + } + } + } + + @OnClick(R.id.connectingRelativeLayoutView) + fun onConnectingViewClick() { + if (currentCallStatus == CallStatus.CALLING_TIMEOUT) { + setCallState(CallStatus.RECONNECTING) + hangupNetworkCalls(false) + } + } + + private fun setCallState(callState: CallStatus) { + if (currentCallStatus == null || currentCallStatus != callState) { + currentCallStatus = callState + if (handler == null) { + handler = Handler(Looper.getMainLooper()) + } else { + handler!!.removeCallbacksAndMessages(null) + } + + when (callState) { + CallController.CallStatus.CALLING -> handler!!.post { + playCallingSound() + connectingTextView!!.setText(R.string.nc_connecting_call) + if (connectingView!!.visibility != View.VISIBLE) { + connectingView!!.visibility = View.VISIBLE + } + + if (conversationView!!.visibility != View.INVISIBLE) { + conversationView!!.visibility = View.INVISIBLE + } + + if (progressBar!!.visibility != View.VISIBLE) { + progressBar!!.visibility = View.VISIBLE + } + + if (errorImageView!!.visibility != View.GONE) { + errorImageView!!.visibility = View.GONE + } + } + CallController.CallStatus.CALLING_TIMEOUT -> handler!!.post { + hangup(false) + connectingTextView!!.setText(R.string.nc_call_timeout) + if (connectingView!!.visibility != View.VISIBLE) { + connectingView!!.visibility = View.VISIBLE + } + + if (progressBar!!.visibility != View.GONE) { + progressBar!!.visibility = View.GONE + } + + if (conversationView!!.visibility != View.INVISIBLE) { + conversationView!!.visibility = View.INVISIBLE + } + + errorImageView!!.setImageResource(R.drawable.ic_av_timer_timer_24dp) + + if (errorImageView!!.visibility != View.VISIBLE) { + errorImageView!!.visibility = View.VISIBLE + } + } + CallController.CallStatus.RECONNECTING -> handler!!.post { + playCallingSound() + connectingTextView!!.setText(R.string.nc_call_reconnecting) + if (connectingView!!.visibility != View.VISIBLE) { + connectingView!!.visibility = View.VISIBLE + } + if (conversationView!!.visibility != View.INVISIBLE) { + conversationView!!.visibility = View.INVISIBLE + } + if (progressBar!!.visibility != View.VISIBLE) { + progressBar!!.visibility = View.VISIBLE + } + + if (errorImageView!!.visibility != View.GONE) { + errorImageView!!.visibility = View.GONE + } + } + CallController.CallStatus.ESTABLISHED -> { + handler!!.postDelayed({ setCallState(CallStatus.CALLING_TIMEOUT) }, 45000) + handler!!.post { + if (connectingView != null) { + connectingTextView!!.setText(R.string.nc_calling) + if (connectingTextView!!.visibility != View.VISIBLE) { + connectingView!!.visibility = View.VISIBLE + } + } + + if (progressBar != null) { + if (progressBar!!.visibility != View.VISIBLE) { + progressBar!!.visibility = View.VISIBLE + } + } + + if (conversationView != null) { + if (conversationView!!.visibility != View.INVISIBLE) { + conversationView!!.visibility = View.INVISIBLE + } + } + + if (errorImageView != null) { + if (errorImageView!!.visibility != View.GONE) { + errorImageView!!.visibility = View.GONE + } + } + } + } + CallController.CallStatus.IN_CONVERSATION -> handler!!.post { + stopCallingSound() + + if (!isPTTActive) { + animateCallControls(false, 5000) + } + + if (connectingView != null) { + if (connectingView!!.visibility != View.INVISIBLE) { + connectingView!!.visibility = View.INVISIBLE + } + } + + if (progressBar != null) { + if (progressBar!!.visibility != View.GONE) { + progressBar!!.visibility = View.GONE + } + } + + if (conversationView != null) { + if (conversationView!!.visibility != View.VISIBLE) { + conversationView!!.visibility = View.VISIBLE + } + } + + if (errorImageView != null) { + if (errorImageView!!.visibility != View.GONE) { + errorImageView!!.visibility = View.GONE + } + } + } + CallController.CallStatus.OFFLINE -> handler!!.post { + stopCallingSound() + + if (connectingTextView != null) { + connectingTextView!!.setText(R.string.nc_offline) + + if (connectingView!!.visibility != View.VISIBLE) { + connectingView!!.visibility = View.VISIBLE + } + } + + if (conversationView != null) { + if (conversationView!!.visibility != View.INVISIBLE) { + conversationView!!.visibility = View.INVISIBLE + } + } + + if (progressBar != null) { + if (progressBar!!.visibility != View.GONE) { + progressBar!!.visibility = View.GONE + } + } + + if (errorImageView != null) { + errorImageView!!.setImageResource(R.drawable.ic_signal_wifi_off_white_24dp) + if (errorImageView!!.visibility != View.VISIBLE) { + errorImageView!!.visibility = View.VISIBLE + } + } + } + CallController.CallStatus.LEAVING -> handler!!.post { + if (!isDestroyed && !isBeingDestroyed) { + stopCallingSound() + connectingTextView!!.setText(R.string.nc_leaving_call) + connectingView!!.visibility = View.VISIBLE + conversationView!!.visibility = View.INVISIBLE + progressBar!!.visibility = View.VISIBLE + errorImageView!!.visibility = View.GONE + } + } + } + } + } + + private fun playCallingSound() { + stopCallingSound() + val ringtoneUri = Uri.parse( + "android.resource://" + + applicationContext!!.packageName + + "/raw/librem_by_feandesign_call" + ) + if (activity != null) { + mediaPlayer = MediaPlayer() + try { + mediaPlayer!!.setDataSource(context, ringtoneUri) + mediaPlayer!!.isLooping = true + val audioAttributes = AudioAttributes.Builder() + .setContentType( + AudioAttributes + .CONTENT_TYPE_SONIFICATION + ) + .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) + .build() + mediaPlayer!!.setAudioAttributes(audioAttributes) + + mediaPlayer!!.setOnPreparedListener { mp -> mediaPlayer!!.start() } + + mediaPlayer!!.prepareAsync() + } catch (e: IOException) { + Log.e(TAG, "Failed to play sound") + } + + } + } + + private fun stopCallingSound() { + if (mediaPlayer != null) { + if (mediaPlayer!!.isPlaying) { + mediaPlayer!!.stop() + } + + mediaPlayer!!.release() + mediaPlayer = null + } + } + + @Subscribe(threadMode = ThreadMode.BACKGROUND) + fun onMessageEvent(networkEvent: NetworkEvent) { + if (networkEvent.networkConnectionEvent == NetworkEvent.NetworkConnectionEvent.NETWORK_CONNECTED) { + if (handler != null) { + handler!!.removeCallbacksAndMessages(null) + } + + /*if (!hasMCU) { + setCallState(CallStatus.RECONNECTING); + hangupNetworkCalls(false); + }*/ + + } else if (networkEvent.networkConnectionEvent == NetworkEvent.NetworkConnectionEvent.NETWORK_DISCONNECTED) { + if (handler != null) { + handler!!.removeCallbacksAndMessages(null) + } + + /* if (!hasMCU) { + setCallState(CallStatus.OFFLINE); + hangup(false); + }*/ + } + } + + @Parcel + enum class CallStatus { + CALLING, + CALLING_TIMEOUT, + ESTABLISHED, + IN_CONVERSATION, + RECONNECTING, + OFFLINE, + LEAVING, + PUBLISHER_FAILED + } + + private inner class MicrophoneButtonTouchListener : View.OnTouchListener { + + @SuppressLint("ClickableViewAccessibility") + override fun onTouch( + v: View, + event: MotionEvent + ): Boolean { + v.onTouchEvent(event) + if (event.action == MotionEvent.ACTION_UP && isPTTActive) { + isPTTActive = false + microphoneControlButton!!.hierarchy + .setPlaceholderImage(R.drawable.ic_mic_off_white_24px) + pulseAnimation!!.stop() + toggleMedia(false, false) + animateCallControls(false, 5000) + } + return true + } + } + + private inner class VideoClickListener : View.OnClickListener { + + override fun onClick(v: View) { + showCallControls() + } + } + + companion object { + + private val TAG = "CallController" + + private val PERMISSIONS_CALL = + arrayOf(android.Manifest.permission.CAMERA, android.Manifest.permission.RECORD_AUDIO) + + private val PERMISSIONS_CAMERA = arrayOf(Manifest.permission.CAMERA) + + private val PERMISSIONS_MICROPHONE = arrayOf(Manifest.permission.RECORD_AUDIO) + } +} 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 efc7a5e54..6b68dbf89 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt @@ -73,7 +73,6 @@ import com.nextcloud.talk.components.filebrowser.controllers.BrowserController import com.nextcloud.talk.controllers.base.BaseController import com.nextcloud.talk.events.UserMentionClickEvent 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 @@ -81,8 +80,10 @@ 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.newarch.local.models.UserNgEntity +import com.nextcloud.talk.newarch.local.models.getCredentials +import com.nextcloud.talk.newarch.local.models.maxMessageLength import com.nextcloud.talk.newarch.utils.Images -import com.nextcloud.talk.newarch.utils.getCredentials import com.nextcloud.talk.presenters.MentionAutocompletePresenter import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.ConductorRemapping @@ -94,7 +95,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.singletons.ApplicationWideCurrentRoomHolder import com.nextcloud.talk.utils.text.Spans import com.nextcloud.talk.webrtc.MagicWebSocketInstance import com.nextcloud.talk.webrtc.WebSocketConnectionHelper @@ -161,7 +161,7 @@ class ChatController(args: Bundle) : BaseController(), MessagesListAdapter @JvmField var conversationLobbyText: TextView? = null var roomToken: String? = null - val conversationUser: UserEntity? + val conversationUser: UserNgEntity? val roomPassword: String var credentials: String? = null var currentConversation: Conversation? = null @@ -459,7 +459,7 @@ class ChatController(args: Bundle) : BaseController(), MessagesListAdapter }) val filters = arrayOfNulls(1) - val lengthFilter = conversationUser?.messageMaxLength ?: 1000 + val lengthFilter = conversationUser?.maxMessageLength() ?: 1000 filters[0] = InputFilter.LengthFilter(lengthFilter) @@ -627,7 +627,7 @@ class ChatController(args: Bundle) : BaseController(), MessagesListAdapter bundle.putParcelable( BundleKeys.KEY_BROWSER_TYPE, Parcels.wrap(browserType) ) - bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, Parcels.wrap(conversationUser)) + bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, Parcels.wrap(conversationUser)) bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomToken) router.pushController( RouterTransaction.with(BrowserController(bundle)) @@ -682,14 +682,6 @@ class ChatController(args: Bundle) : BaseController(), MessagesListAdapter } isLeavingForConversation = false - ApplicationWideCurrentRoomHolder.getInstance() - .currentRoomId = roomId - ApplicationWideCurrentRoomHolder.getInstance() - .currentRoomToken = roomId - ApplicationWideCurrentRoomHolder.getInstance() - .isInCall = false - ApplicationWideCurrentRoomHolder.getInstance() - .userInRoom = conversationUser isLinkPreviewAllowed = appPreferences.areLinkPreviewsAllowed @@ -745,8 +737,6 @@ class ChatController(args: Bundle) : BaseController(), MessagesListAdapter override fun onDetach(view: View) { eventBus.unregister(this) - ApplicationWideCurrentRoomHolder.getInstance() - .clear() if (activity != null) { activity?.findViewById(R.id.toolbar) @@ -850,10 +840,6 @@ class ChatController(args: Bundle) : BaseController(), MessagesListAdapter override fun onNext(roomOverall: RoomOverall) { inConversation = true currentConversation?.sessionId = roomOverall.ocs.data.sessionId - - ApplicationWideCurrentRoomHolder.getInstance() - .session = - currentConversation?.sessionId startPing() setupWebsocket() @@ -886,8 +872,6 @@ class ChatController(args: Bundle) : BaseController(), MessagesListAdapter }) } else { inConversation = true - ApplicationWideCurrentRoomHolder.getInstance() - .session = currentConversation?.sessionId if (magicWebSocketInstance != null) { magicWebSocketInstance?.joinRoomWithRoomTokenAndSession( roomToken, @@ -1198,7 +1182,7 @@ class ChatController(args: Bundle) : BaseController(), MessagesListAdapter chatMessageList[i + 1].createdAt ) ) { - chatMessageList[i].isGrouped = true + chatMessageList[i].grouped = true countGroupedMessages++ } else { countGroupedMessages = 0 @@ -1206,7 +1190,7 @@ class ChatController(args: Bundle) : BaseController(), MessagesListAdapter } val chatMessage = chatMessageList[i] - chatMessage.isOneToOneConversation = + chatMessage.oneToOneConversation = currentConversation?.type == Conversation.ConversationType.ONE_TO_ONE_CONVERSATION chatMessage.isLinkPreviewAllowed = isLinkPreviewAllowed chatMessage.activeUser = conversationUser @@ -1272,11 +1256,11 @@ class ChatController(args: Bundle) : BaseController(), MessagesListAdapter } if (adapter != null) { - chatMessage.isGrouped = (adapter!!.isPreviousSameAuthor( + chatMessage.grouped = (adapter!!.isPreviousSameAuthor( chatMessage .actorId, -1 ) && adapter!!.getSameAuthorLastMessagesCount(chatMessage.actorId) % 5 > 0) - chatMessage.isOneToOneConversation = + chatMessage.oneToOneConversation = (currentConversation?.type == Conversation.ConversationType.ONE_TO_ONE_CONVERSATION) adapter?.addToStart(chatMessage, shouldScroll) } diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ContactsController.java b/app/src/main/java/com/nextcloud/talk/controllers/ContactsController.java deleted file mode 100644 index a9f935a58..000000000 --- a/app/src/main/java/com/nextcloud/talk/controllers/ContactsController.java +++ /dev/null @@ -1,1051 +0,0 @@ -/* - * Nextcloud Talk application - * - * @author Mario Danic - * Copyright (C) 2017 Mario Danic (mario@lovelyhq.com) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.nextcloud.talk.controllers; - -import android.app.SearchManager; -import android.content.Context; -import android.content.Intent; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.text.InputType; -import android.text.TextUtils; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.view.inputmethod.EditorInfo; -import android.widget.ProgressBar; -import android.widget.RelativeLayout; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.widget.SearchView; -import androidx.coordinatorlayout.widget.CoordinatorLayout; -import androidx.core.view.MenuItemCompat; -import androidx.recyclerview.widget.RecyclerView; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; -import androidx.work.Data; -import androidx.work.OneTimeWorkRequest; -import androidx.work.WorkManager; -import autodagger.AutoInjector; -import butterknife.BindView; -import butterknife.OnClick; -import butterknife.Optional; -import com.bluelinelabs.conductor.RouterTransaction; -import com.bluelinelabs.conductor.changehandler.VerticalChangeHandler; -import com.bluelinelabs.logansquare.LoganSquare; -import com.kennyc.bottomsheet.BottomSheet; -import com.nextcloud.talk.R; -import com.nextcloud.talk.activities.MagicCallActivity; -import com.nextcloud.talk.adapters.items.GenericTextHeaderItem; -import com.nextcloud.talk.adapters.items.ProgressItem; -import com.nextcloud.talk.adapters.items.UserItem; -import com.nextcloud.talk.api.NcApi; -import com.nextcloud.talk.application.NextcloudTalkApplication; -import com.nextcloud.talk.controllers.base.BaseController; -import com.nextcloud.talk.controllers.bottomsheet.EntryMenuController; -import com.nextcloud.talk.controllers.bottomsheet.OperationsMenuController; -import com.nextcloud.talk.events.BottomSheetLockEvent; -import com.nextcloud.talk.jobs.AddParticipantsToConversation; -import com.nextcloud.talk.models.RetrofitBucket; -import com.nextcloud.talk.models.database.UserEntity; -import com.nextcloud.talk.models.json.autocomplete.AutocompleteOverall; -import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser; -import com.nextcloud.talk.models.json.conversations.Conversation; -import com.nextcloud.talk.models.json.conversations.RoomOverall; -import com.nextcloud.talk.models.json.participants.Participant; -import com.nextcloud.talk.models.json.sharees.Sharee; -import com.nextcloud.talk.models.json.sharees.ShareesOverall; -import com.nextcloud.talk.utils.ApiUtils; -import com.nextcloud.talk.utils.ConductorRemapping; -import com.nextcloud.talk.utils.KeyboardUtils; -import com.nextcloud.talk.utils.bundle.BundleKeys; -import com.nextcloud.talk.utils.database.user.UserUtils; -import com.nextcloud.talk.utils.preferences.AppPreferences; -import com.uber.autodispose.AutoDispose; -import eu.davidea.fastscroller.FastScroller; -import eu.davidea.flexibleadapter.FlexibleAdapter; -import eu.davidea.flexibleadapter.SelectableAdapter; -import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager; -import eu.davidea.flexibleadapter.items.AbstractFlexibleItem; -import eu.davidea.flexibleadapter.items.IFlexible; -import io.reactivex.Observer; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import javax.inject.Inject; -import okhttp3.ResponseBody; -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; -import org.parceler.Parcels; - -@AutoInjector(NextcloudTalkApplication.class) -public class ContactsController extends BaseController implements SearchView.OnQueryTextListener, - FlexibleAdapter.OnItemClickListener, FastScroller.OnScrollStateChangeListener, - FlexibleAdapter.EndlessScrollListener { - - public static final String TAG = "ContactsController"; - - @Nullable - @BindView(R.id.initial_relative_layout) - RelativeLayout initialRelativeLayout; - @Nullable - @BindView(R.id.secondary_relative_layout) - RelativeLayout secondaryRelativeLayout; - @Inject - UserUtils userUtils; - @Inject - EventBus eventBus; - @Inject - AppPreferences appPreferences; - @BindView(R.id.progressBar) - ProgressBar progressBar; - @BindView(R.id.recyclerView) - RecyclerView recyclerView; - - @BindView(R.id.swipe_refresh_layout) - SwipeRefreshLayout swipeRefreshLayout; - - @BindView(R.id.fast_scroller) - FastScroller fastScroller; - - @BindView(R.id.call_header_layout) - RelativeLayout conversationPrivacyToogleLayout; - - @BindView(R.id.joinConversationViaLinkRelativeLayout) - RelativeLayout joinConversationViaLinkLayout; - - @BindView(R.id.generic_rv_layout) - CoordinatorLayout genericRvLayout; - - @Inject - NcApi ncApi; - private String credentials; - private UserEntity currentUser; - private FlexibleAdapter adapter; - private List contactItems; - private BottomSheet bottomSheet; - private View view; - private int currentPage; - private int currentSearchPage; - - private SmoothScrollLinearLayoutManager layoutManager; - - private MenuItem searchItem; - private SearchView searchView; - - private boolean isNewConversationView; - private boolean isPublicCall; - - private HashMap userHeaderItems = new HashMap<>(); - - private boolean alreadyFetching = false; - private boolean canFetchFurther = true; - private boolean canFetchSearchFurther = true; - - private MenuItem doneMenuItem; - - private Set selectedUserIds; - private Set selectedGroupIds; - private List existingParticipants; - private boolean isAddingParticipantsView; - private String conversationToken; - - public ContactsController() { - super(); - setHasOptionsMenu(true); - } - - public ContactsController(Bundle args) { - super(); - setHasOptionsMenu(true); - if (args.containsKey(BundleKeys.INSTANCE.getKEY_NEW_CONVERSATION())) { - isNewConversationView = true; - existingParticipants = new ArrayList<>(); - } else if (args.containsKey(BundleKeys.INSTANCE.getKEY_ADD_PARTICIPANTS())) { - isAddingParticipantsView = true; - conversationToken = args.getString(BundleKeys.INSTANCE.getKEY_TOKEN()); - - existingParticipants = new ArrayList<>(); - - if (args.containsKey(BundleKeys.INSTANCE.getKEY_EXISTING_PARTICIPANTS())) { - existingParticipants = - args.getStringArrayList(BundleKeys.INSTANCE.getKEY_EXISTING_PARTICIPANTS()); - } - } - - selectedGroupIds = new HashSet<>(); - selectedUserIds = new HashSet<>(); - } - - @Override - protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) { - return inflater.inflate(R.layout.controller_contacts_rv, container, false); - } - - @Override - protected void onDetach(@NonNull View view) { - eventBus.unregister(this); - super.onDetach(view); - } - - @Override - protected void onAttach(@NonNull View view) { - super.onAttach(view); - eventBus.register(this); - - if (isNewConversationView) { - toggleNewCallHeaderVisibility(!isPublicCall); - } - - if (isAddingParticipantsView) { - joinConversationViaLinkLayout.setVisibility(View.GONE); - conversationPrivacyToogleLayout.setVisibility(View.GONE); - } - } - - @Override - protected void onViewBound(@NonNull View view) { - super.onViewBound(view); - NextcloudTalkApplication.Companion.getSharedApplication() - .getComponentApplication() - .inject(this); - - currentUser = userUtils.getCurrentUser(); - - if (currentUser != null) { - credentials = ApiUtils.getCredentials(currentUser.getUsername(), currentUser.getToken()); - } - - if (adapter == null) { - contactItems = new ArrayList<>(); - adapter = new FlexibleAdapter<>(contactItems, getActivity(), true); - - if (currentUser != null) { - fetchData(true); - } - } - - setupAdapter(); - prepareViews(); - } - - private void setupAdapter() { - adapter.setNotifyChangeOfUnfilteredItems(true) - .setMode(SelectableAdapter.Mode.MULTI); - - adapter.setEndlessScrollListener(this, new ProgressItem()); - - adapter.setStickyHeaderElevation(5) - .setUnlinkAllItemsOnRemoveHeaders(true) - .setDisplayHeadersAtStartUp(true) - .setStickyHeaders(true); - - adapter.addListener(this); - } - - private void selectionDone() { - if (!isAddingParticipantsView) { - if (!isPublicCall && (selectedGroupIds.size() + selectedUserIds.size() == 1)) { - String userId; - String roomType = "1"; - - if (selectedGroupIds.size() == 1) { - roomType = "2"; - userId = selectedGroupIds.iterator().next(); - } else { - userId = selectedUserIds.iterator().next(); - } - - RetrofitBucket retrofitBucket = - ApiUtils.getRetrofitBucketForCreateRoom(currentUser.getBaseUrl(), roomType, - userId, null); - ncApi.createRoom(credentials, - retrofitBucket.getUrl(), retrofitBucket.getQueryMap()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .as(AutoDispose.autoDisposable(getScopeProvider())) - .subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - - } - - @Override - public void onNext(RoomOverall roomOverall) { - Intent conversationIntent = new Intent(getActivity(), MagicCallActivity.class); - Bundle bundle = new Bundle(); - bundle.putParcelable(BundleKeys.INSTANCE.getKEY_USER_ENTITY(), currentUser); - bundle.putString(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN(), - roomOverall.getOcs().getData().getToken()); - bundle.putString(BundleKeys.INSTANCE.getKEY_ROOM_ID(), - roomOverall.getOcs().getData().getConversationId()); - - if (currentUser.hasSpreedFeatureCapability("chat-v2")) { - ncApi.getRoom(credentials, - ApiUtils.getRoom(currentUser.getBaseUrl(), - roomOverall.getOcs().getData().getToken())) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .as(AutoDispose.autoDisposable(getScopeProvider())) - .subscribe(new Observer() { - - @Override - public void onSubscribe(Disposable d) { - - } - - @Override - public void onNext(RoomOverall roomOverall) { - bundle.putParcelable(BundleKeys.INSTANCE.getKEY_ACTIVE_CONVERSATION(), - Parcels.wrap(roomOverall.getOcs().getData())); - - ConductorRemapping.INSTANCE.remapChatController(getRouter(), - currentUser.getId(), - roomOverall.getOcs().getData().getToken(), bundle, true); - } - - @Override - public void onError(Throwable e) { - - } - - @Override - public void onComplete() { - - } - }); - } else { - conversationIntent.putExtras(bundle); - startActivity(conversationIntent); - new Handler().postDelayed(new Runnable() { - @Override - public void run() { - if (!isDestroyed() && !isBeingDestroyed()) { - getRouter().popCurrentController(); - } - } - }, 100); - } - } - - @Override - public void onError(Throwable e) { - - } - - @Override - public void onComplete() { - } - }); - } else { - - Bundle bundle = new Bundle(); - Conversation.ConversationType roomType; - if (isPublicCall) { - roomType = Conversation.ConversationType.PUBLIC_CONVERSATION; - } else { - roomType = Conversation.ConversationType.GROUP_CONVERSATION; - } - - ArrayList userIdsArray = new ArrayList<>(selectedUserIds); - ArrayList groupIdsArray = new ArrayList<>(selectedGroupIds); - - bundle.putParcelable(BundleKeys.INSTANCE.getKEY_CONVERSATION_TYPE(), - Parcels.wrap(roomType)); - bundle.putStringArrayList(BundleKeys.INSTANCE.getKEY_INVITED_PARTICIPANTS(), userIdsArray); - bundle.putStringArrayList(BundleKeys.INSTANCE.getKEY_INVITED_GROUP(), groupIdsArray); - bundle.putInt(BundleKeys.INSTANCE.getKEY_OPERATION_CODE(), 11); - prepareAndShowBottomSheetWithBundle(bundle, true); - } - } else { - String[] userIdsArray = selectedUserIds.toArray(new String[selectedUserIds.size()]); - String[] groupIdsArray = selectedGroupIds.toArray(new String[selectedGroupIds.size()]); - - Data.Builder data = new Data.Builder(); - data.putLong(BundleKeys.INSTANCE.getKEY_INTERNAL_USER_ID(), currentUser.getId()); - data.putString(BundleKeys.INSTANCE.getKEY_TOKEN(), conversationToken); - data.putStringArray(BundleKeys.INSTANCE.getKEY_SELECTED_USERS(), userIdsArray); - data.putStringArray(BundleKeys.INSTANCE.getKEY_SELECTED_GROUPS(), groupIdsArray); - - OneTimeWorkRequest addParticipantsToConversationWorker = - new OneTimeWorkRequest.Builder(AddParticipantsToConversation.class).setInputData( - data.build()).build(); - WorkManager.getInstance().enqueue(addParticipantsToConversationWorker); - - getRouter().popCurrentController(); - } - } - - private void initSearchView() { - if (getActivity() != null) { - SearchManager searchManager = - (SearchManager) getActivity().getSystemService(Context.SEARCH_SERVICE); - if (searchItem != null) { - searchView = (SearchView) MenuItemCompat.getActionView(searchItem); - searchView.setMaxWidth(Integer.MAX_VALUE); - searchView.setInputType(InputType.TYPE_TEXT_VARIATION_FILTER); - int imeOptions = EditorInfo.IME_ACTION_DONE | EditorInfo.IME_FLAG_NO_FULLSCREEN; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O - && appPreferences.getIsKeyboardIncognito()) { - imeOptions |= EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING; - } - searchView.setImeOptions(imeOptions); - searchView.setQueryHint(getResources().getString(R.string.nc_search)); - if (searchManager != null) { - searchView.setSearchableInfo( - searchManager.getSearchableInfo(getActivity().getComponentName())); - } - searchView.setOnQueryTextListener(this); - } - } - } - - @Override - public boolean onOptionsItemSelected(@NonNull MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - getRouter().popCurrentController(); - return true; - case R.id.contacts_selection_done: - selectionDone(); - return true; - default: - return super.onOptionsItemSelected(item); - } - } - - @Override - public void onCreateOptionsMenu(Menu menu, @NonNull MenuInflater inflater) { - super.onCreateOptionsMenu(menu, inflater); - inflater.inflate(R.menu.menu_contacts, menu); - searchItem = menu.findItem(R.id.action_search); - doneMenuItem = menu.findItem(R.id.contacts_selection_done); - - initSearchView(); - } - - @Override - public void onPrepareOptionsMenu(@NonNull Menu menu) { - super.onPrepareOptionsMenu(menu); - checkAndHandleDoneMenuItem(); - if (adapter.hasFilter()) { - searchItem.expandActionView(); - searchView.setQuery((CharSequence) adapter.getFilter(String.class), false); - } - } - - private void fetchData(boolean startFromScratch) { - alreadyFetching = true; - Set shareeHashSet = new HashSet<>(); - Set autocompleteUsersHashSet = new HashSet<>(); - - userHeaderItems = new HashMap<>(); - - String query = (String) adapter.getFilter(String.class); - - RetrofitBucket retrofitBucket; - boolean serverIs14OrUp = false; - if (currentUser.hasSpreedFeatureCapability("last-room-activity")) { - // a hack to see if we're on 14 or not - retrofitBucket = - ApiUtils.getRetrofitBucketForContactsSearchFor14(currentUser.getBaseUrl(), query); - serverIs14OrUp = true; - } else { - retrofitBucket = ApiUtils.getRetrofitBucketForContactsSearch(currentUser.getBaseUrl(), query); - } - - int page = 1; - if (!startFromScratch) { - if (TextUtils.isEmpty(query)) { - page = currentPage + 1; - } else { - page = currentSearchPage + 1; - } - } - - Map modifiedQueryMap = new HashMap<>(retrofitBucket.getQueryMap()); - modifiedQueryMap.put("page", page); - modifiedQueryMap.put("perPage", 100); - - List shareTypesList = null; - - if (serverIs14OrUp) { - shareTypesList = new ArrayList<>(); - // users - shareTypesList.add("0"); - // groups - shareTypesList.add("1"); - // mails - //shareTypesList.add("4"); - - modifiedQueryMap.put("shareTypes[]", shareTypesList); - } - - boolean finalServerIs14OrUp = serverIs14OrUp; - ncApi.getContactsWithSearchParam( - credentials, - retrofitBucket.getUrl(), shareTypesList, modifiedQueryMap) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .retry(3) - .as(AutoDispose.autoDisposable(getScopeProvider())) - .subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - } - - @Override - public void onNext(ResponseBody responseBody) { - if (responseBody != null) { - Participant participant; - - List newUserItemList = new ArrayList<>(); - - try { - if (!finalServerIs14OrUp) { - ShareesOverall shareesOverall = - LoganSquare.parse(responseBody.string(), ShareesOverall.class); - - if (shareesOverall.getOcs().getData().getUsers() != null) { - shareeHashSet.addAll(shareesOverall.getOcs().getData().getUsers()); - } - - if (shareesOverall.getOcs().getData().getExactUsers() != null && - shareesOverall.getOcs().getData().getExactUsers().getExactSharees() != null) { - shareeHashSet.addAll(shareesOverall.getOcs().getData(). - getExactUsers().getExactSharees()); - } - - for (Sharee sharee : shareeHashSet) { - if (!sharee.getValue().getShareWith().equals(currentUser.getUserId()) - && !existingParticipants.contains(sharee.getValue().getShareWith())) { - participant = new Participant(); - participant.setDisplayName(sharee.getLabel()); - String headerTitle; - - headerTitle = sharee.getLabel().substring(0, 1).toUpperCase(); - - GenericTextHeaderItem genericTextHeaderItem; - if (!userHeaderItems.containsKey(headerTitle)) { - genericTextHeaderItem = new GenericTextHeaderItem(headerTitle); - userHeaderItems.put(headerTitle, genericTextHeaderItem); - } - - participant.setUserId(sharee.getValue().getShareWith()); - - UserItem newContactItem = new UserItem(participant, currentUser, - userHeaderItems.get(headerTitle), getActivity()); - - if (!contactItems.contains(newContactItem)) { - newUserItemList.add(newContactItem); - } - } - } - } else { - AutocompleteOverall autocompleteOverall = - LoganSquare.parse(responseBody.string(), AutocompleteOverall.class); - autocompleteUsersHashSet.addAll(autocompleteOverall.getOcs().getData()); - - for (AutocompleteUser autocompleteUser : autocompleteUsersHashSet) { - if (!autocompleteUser.getId().equals(currentUser.getUserId()) - && !existingParticipants.contains(autocompleteUser.getId())) { - participant = new Participant(); - participant.setUserId(autocompleteUser.getId()); - participant.setDisplayName(autocompleteUser.getLabel()); - participant.setSource(autocompleteUser.getSource()); - - String headerTitle; - - if (!autocompleteUser.getSource().equals("groups")) { - headerTitle = participant.getDisplayName().substring(0, 1).toUpperCase(); - } else { - headerTitle = getResources().getString(R.string.nc_groups); - } - - GenericTextHeaderItem genericTextHeaderItem; - if (!userHeaderItems.containsKey(headerTitle)) { - genericTextHeaderItem = new GenericTextHeaderItem(headerTitle); - userHeaderItems.put(headerTitle, genericTextHeaderItem); - } - - UserItem newContactItem = new UserItem(participant, currentUser, - userHeaderItems.get(headerTitle), getActivity()); - - if (!contactItems.contains(newContactItem)) { - newUserItemList.add(newContactItem); - } - } - } - } - } catch (Exception exception) { - Log.e(TAG, "Parsing response body failed while getting contacts"); - } - - if (TextUtils.isEmpty((CharSequence) modifiedQueryMap.get("search"))) { - canFetchFurther = !shareeHashSet.isEmpty() || (finalServerIs14OrUp - && autocompleteUsersHashSet.size() == 100); - currentPage = (int) modifiedQueryMap.get("page"); - } else { - canFetchSearchFurther = !shareeHashSet.isEmpty() || (finalServerIs14OrUp - && autocompleteUsersHashSet.size() == 100); - currentSearchPage = (int) modifiedQueryMap.get("page"); - } - - userHeaderItems = new HashMap<>(); - contactItems.addAll(newUserItemList); - - Collections.sort(newUserItemList, (o1, o2) -> { - String firstName; - String secondName; - - if (o1 instanceof UserItem) { - firstName = ((UserItem) o1).getModel().getDisplayName(); - } else { - firstName = ((GenericTextHeaderItem) o1).getModel(); - } - - if (o2 instanceof UserItem) { - secondName = ((UserItem) o2).getModel().getDisplayName(); - } else { - secondName = ((GenericTextHeaderItem) o2).getModel(); - } - - if (o1 instanceof UserItem && o2 instanceof UserItem) { - if ("groups".equals(((UserItem) o1).getModel().getSource()) && "groups".equals( - ((UserItem) o2).getModel().getSource())) { - return firstName.compareToIgnoreCase(secondName); - } else if ("groups".equals(((UserItem) o1).getModel().getSource())) { - return -1; - } else if ("groups".equals(((UserItem) o2).getModel().getSource())) { - return 1; - } - } - - return firstName.compareToIgnoreCase(secondName); - }); - - Collections.sort(contactItems, (o1, o2) -> { - String firstName; - String secondName; - - if (o1 instanceof UserItem) { - firstName = ((UserItem) o1).getModel().getDisplayName(); - } else { - firstName = ((GenericTextHeaderItem) o1).getModel(); - } - - if (o2 instanceof UserItem) { - secondName = ((UserItem) o2).getModel().getDisplayName(); - } else { - secondName = ((GenericTextHeaderItem) o2).getModel(); - } - - if (o1 instanceof UserItem && o2 instanceof UserItem) { - if ("groups".equals(((UserItem) o1).getModel().getSource()) && "groups".equals( - ((UserItem) o2).getModel().getSource())) { - return firstName.compareToIgnoreCase(secondName); - } else if ("groups".equals(((UserItem) o1).getModel().getSource())) { - return -1; - } else if ("groups".equals(((UserItem) o2).getModel().getSource())) { - return 1; - } - } - - return firstName.compareToIgnoreCase(secondName); - }); - - if (newUserItemList.size() > 0) { - adapter.updateDataSet(newUserItemList); - } else { - adapter.filterItems(); - } - - if (swipeRefreshLayout != null) { - swipeRefreshLayout.setRefreshing(false); - } - } - } - - @Override - public void onError(Throwable e) { - if (swipeRefreshLayout != null) { - swipeRefreshLayout.setRefreshing(false); - } - } - - @Override - public void onComplete() { - if (swipeRefreshLayout != null) { - swipeRefreshLayout.setRefreshing(false); - } - alreadyFetching = false; - - disengageProgressBar(); - } - }); - } - - private void prepareViews() { - layoutManager = new SmoothScrollLinearLayoutManager(getActivity()); - recyclerView.setLayoutManager(layoutManager); - recyclerView.setHasFixedSize(true); - recyclerView.setAdapter(adapter); - - swipeRefreshLayout.setOnRefreshListener(() -> fetchData(true)); - swipeRefreshLayout.setColorSchemeResources(R.color.colorPrimary); - - fastScroller.addOnScrollStateChangeListener(this); - adapter.setFastScroller(fastScroller); - fastScroller.setBubbleTextCreator(position -> { - IFlexible abstractFlexibleItem = adapter.getItem(position); - if (abstractFlexibleItem instanceof UserItem) { - return ((UserItem) adapter.getItem(position)).getHeader().getModel(); - } else if (abstractFlexibleItem instanceof GenericTextHeaderItem) { - return ((GenericTextHeaderItem) adapter.getItem(position)).getModel(); - } else { - return ""; - } - }); - - disengageProgressBar(); - } - - private void disengageProgressBar() { - if (!alreadyFetching) { - progressBar.setVisibility(View.GONE); - genericRvLayout.setVisibility(View.VISIBLE); - - if (isNewConversationView) { - conversationPrivacyToogleLayout.setVisibility(View.VISIBLE); - joinConversationViaLinkLayout.setVisibility(View.VISIBLE); - } - } - } - - @Override - public void onSaveViewState(@NonNull View view, @NonNull Bundle outState) { - adapter.onSaveInstanceState(outState); - super.onSaveViewState(view, outState); - } - - @Override - public void onRestoreViewState(@NonNull View view, @NonNull Bundle savedViewState) { - super.onRestoreViewState(view, savedViewState); - if (adapter != null) { - adapter.onRestoreInstanceState(savedViewState); - } - } - - @Override - public boolean onQueryTextChange(String newText) { - if (!newText.equals("") && adapter.hasNewFilter(newText)) { - adapter.setFilter(newText); - fetchData(true); - } else if (newText.equals("")) { - adapter.setFilter(""); - adapter.updateDataSet(contactItems); - } - - if (swipeRefreshLayout != null) { - swipeRefreshLayout.setEnabled(!adapter.hasFilter()); - } - - return true; - } - - @Override - public boolean onQueryTextSubmit(String query) { - return onQueryTextChange(query); - } - - private void checkAndHandleDoneMenuItem() { - if (adapter != null && doneMenuItem != null) { - if ((selectedGroupIds.size() + selectedUserIds.size() > 0) || isPublicCall) { - doneMenuItem.setVisible(true); - } else { - doneMenuItem.setVisible(false); - } - } else if (doneMenuItem != null) { - doneMenuItem.setVisible(false); - } - } - - @Override - public String getTitle() { - if (!isNewConversationView && !isAddingParticipantsView) { - return getResources().getString(R.string.nc_app_name); - } else { - return getResources().getString(R.string.nc_select_contacts); - } - } - - @Override - public void onFastScrollerStateChange(boolean scrolling) { - swipeRefreshLayout.setEnabled(!scrolling); - } - - private void prepareAndShowBottomSheetWithBundle(Bundle bundle, boolean showEntrySheet) { - if (view == null) { - view = getActivity().getLayoutInflater().inflate(R.layout.bottom_sheet, null, false); - } - - if (bottomSheet == null) { - bottomSheet = new BottomSheet.Builder(getActivity()).setView(view).create(); - } - - if (showEntrySheet) { - getChildRouter((ViewGroup) view).setRoot( - RouterTransaction.with(new EntryMenuController(bundle)) - .popChangeHandler(new VerticalChangeHandler()) - .pushChangeHandler(new VerticalChangeHandler())); - } else { - getChildRouter((ViewGroup) view).setRoot( - RouterTransaction.with(new OperationsMenuController(bundle)) - .popChangeHandler(new VerticalChangeHandler()) - .pushChangeHandler(new VerticalChangeHandler())); - } - - bottomSheet.setOnShowListener(dialog -> { - if (showEntrySheet) { - new KeyboardUtils(getActivity(), bottomSheet.getLayout(), true); - } else { - eventBus.post(new BottomSheetLockEvent(false, 0, - false, false)); - } - }); - - bottomSheet.setOnDismissListener( - dialog -> getActionBar().setDisplayHomeAsUpEnabled(getRouter().getBackstackSize() > 1)); - - bottomSheet.show(); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onMessageEvent(BottomSheetLockEvent bottomSheetLockEvent) { - - if (bottomSheet != null) { - if (!bottomSheetLockEvent.isCancelable()) { - bottomSheet.setCancelable(bottomSheetLockEvent.isCancelable()); - } else { - bottomSheet.setCancelable(bottomSheetLockEvent.isCancelable()); - if (bottomSheet.isShowing() && bottomSheetLockEvent.isCancel()) { - new Handler().postDelayed(() -> { - bottomSheet.setOnCancelListener(null); - bottomSheet.cancel(); - }, bottomSheetLockEvent.getDelay()); - } - } - } - } - - @Override - public boolean onItemClick(View view, int position) { - if (adapter.getItem(position) instanceof UserItem) { - if (!isNewConversationView && !isAddingParticipantsView) { - UserItem userItem = (UserItem) adapter.getItem(position); - String roomType = "1"; - - if ("groups".equals(userItem.getModel().getSource())) { - roomType = "2"; - } - - RetrofitBucket retrofitBucket = - ApiUtils.getRetrofitBucketForCreateRoom(currentUser.getBaseUrl(), roomType, - userItem.getModel().getUserId(), null); - - ncApi.createRoom(credentials, - retrofitBucket.getUrl(), retrofitBucket.getQueryMap()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .as(AutoDispose.autoDisposable(getScopeProvider())) - .subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - - } - - @Override - public void onNext(RoomOverall roomOverall) { - if (getActivity() != null) { - Intent conversationIntent = new Intent(getActivity(), MagicCallActivity.class); - Bundle bundle = new Bundle(); - bundle.putParcelable(BundleKeys.INSTANCE.getKEY_USER_ENTITY(), currentUser); - bundle.putString(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN(), - roomOverall.getOcs().getData().getToken()); - bundle.putString(BundleKeys.INSTANCE.getKEY_ROOM_ID(), - roomOverall.getOcs().getData().getConversationId()); - conversationIntent.putExtras(bundle); - - if (currentUser.hasSpreedFeatureCapability("chat-v2")) { - bundle.putParcelable(BundleKeys.INSTANCE.getKEY_ACTIVE_CONVERSATION(), - Parcels.wrap(roomOverall.getOcs().getData())); - - ConductorRemapping.INSTANCE.remapChatController(getRouter(), - currentUser.getId(), - roomOverall.getOcs().getData().getToken(), bundle, true); - } else { - startActivity(conversationIntent); - new Handler().postDelayed(() -> getRouter().popCurrentController(), 100); - } - } - } - - @Override - public void onError(Throwable e) { - - } - - @Override - public void onComplete() { - - } - }); - } else { - Participant participant = ((UserItem) adapter.getItem(position)).getModel(); - participant.setSelected(!participant.isSelected()); - - if ("groups".equals(participant.getSource())) { - if (participant.isSelected()) { - selectedGroupIds.add(participant.getUserId()); - } else { - selectedGroupIds.remove(participant.getUserId()); - } - } else { - if (participant.isSelected()) { - selectedUserIds.add(participant.getUserId()); - } else { - selectedUserIds.remove(participant.getUserId()); - } - } - - if (currentUser.hasSpreedFeatureCapability("last-room-activity") - && !currentUser.hasSpreedFeatureCapability("invite-groups-and-mails") && - "groups".equals(((UserItem) adapter.getItem(position)).getModel().getSource()) && - participant.isSelected() && - adapter.getSelectedItemCount() > 1) { - List currentItems = adapter.getCurrentItems(); - Participant internalParticipant; - for (int i = 0; i < currentItems.size(); i++) { - internalParticipant = currentItems.get(i).getModel(); - if (internalParticipant.getUserId().equals(participant.getUserId()) - && - "groups".equals(internalParticipant.getSource()) - && internalParticipant.isSelected()) { - internalParticipant.setSelected(false); - selectedGroupIds.remove(internalParticipant.getUserId()); - } - } - } - - adapter.notifyDataSetChanged(); - checkAndHandleDoneMenuItem(); - } - } - return true; - } - - @Optional - @OnClick(R.id.joinConversationViaLinkRelativeLayout) - void joinConversationViaLink() { - Bundle bundle = new Bundle(); - bundle.putInt(BundleKeys.INSTANCE.getKEY_OPERATION_CODE(), 10); - - prepareAndShowBottomSheetWithBundle(bundle, true); - } - - @Optional - @OnClick(R.id.call_header_layout) - void toggleCallHeader() { - toggleNewCallHeaderVisibility(isPublicCall); - isPublicCall = !isPublicCall; - - if (isPublicCall) { - joinConversationViaLinkLayout.setVisibility(View.GONE); - } else { - joinConversationViaLinkLayout.setVisibility(View.VISIBLE); - } - - if (isPublicCall) { - List currentItems = adapter.getCurrentItems(); - Participant internalParticipant; - for (int i = 0; i < currentItems.size(); i++) { - if (currentItems.get(i) instanceof UserItem) { - internalParticipant = ((UserItem) currentItems.get(i)).getModel(); - if ("groups".equals(internalParticipant.getSource()) - && internalParticipant.isSelected()) { - internalParticipant.setSelected(false); - selectedGroupIds.remove(internalParticipant.getUserId()); - } - } - } - } - - for (int i = 0; i < adapter.getItemCount(); i++) { - if (adapter.getItem(i) instanceof UserItem) { - UserItem userItem = (UserItem) adapter.getItem(i); - if ("groups".equals(userItem.getModel().getSource())) { - userItem.setEnabled(!isPublicCall); - } - } - } - - checkAndHandleDoneMenuItem(); - adapter.notifyDataSetChanged(); - } - - private void toggleNewCallHeaderVisibility(boolean showInitialLayout) { - if (showInitialLayout) { - initialRelativeLayout.setVisibility(View.VISIBLE); - secondaryRelativeLayout.setVisibility(View.GONE); - } else { - initialRelativeLayout.setVisibility(View.GONE); - secondaryRelativeLayout.setVisibility(View.VISIBLE); - } - } - - @Override - public void noMoreLoad(int newItemsSize) { - } - - @Override - public void onLoadMore(int lastPosition, int currentPage) { - String query = (String) adapter.getFilter(String.class); - - if (!alreadyFetching && ((searchView != null && searchView.isIconified() && canFetchFurther) - || (!TextUtils.isEmpty(query) && canFetchSearchFurther))) { - fetchData(false); - } else { - adapter.onLoadMoreComplete(null); - } - } -} diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ContactsController.kt b/app/src/main/java/com/nextcloud/talk/controllers/ContactsController.kt new file mode 100644 index 000000000..210205cd9 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/controllers/ContactsController.kt @@ -0,0 +1,1041 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * Copyright (C) 2017 Mario Danic (mario@lovelyhq.com) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.controllers + +import android.app.SearchManager +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.os.Handler +import android.text.InputType +import android.text.TextUtils +import android.util.Log +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.EditorInfo +import android.widget.ProgressBar +import android.widget.RelativeLayout +import androidx.appcompat.widget.SearchView +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.view.MenuItemCompat +import androidx.recyclerview.widget.RecyclerView +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import androidx.work.Data +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkManager +import autodagger.AutoInjector +import butterknife.BindView +import butterknife.OnClick +import butterknife.Optional +import com.bluelinelabs.conductor.RouterTransaction +import com.bluelinelabs.conductor.changehandler.VerticalChangeHandler +import com.bluelinelabs.logansquare.LoganSquare +import com.kennyc.bottomsheet.BottomSheet +import com.nextcloud.talk.R +import com.nextcloud.talk.activities.MagicCallActivity +import com.nextcloud.talk.adapters.items.GenericTextHeaderItem +import com.nextcloud.talk.adapters.items.ProgressItem +import com.nextcloud.talk.adapters.items.UserItem +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.controllers.base.BaseController +import com.nextcloud.talk.controllers.bottomsheet.EntryMenuController +import com.nextcloud.talk.controllers.bottomsheet.OperationsMenuController +import com.nextcloud.talk.events.BottomSheetLockEvent +import com.nextcloud.talk.jobs.AddParticipantsToConversation +import com.nextcloud.talk.models.RetrofitBucket +import com.nextcloud.talk.models.database.User +import com.nextcloud.talk.models.json.autocomplete.AutocompleteOverall +import com.nextcloud.talk.models.json.autocomplete.AutocompleteUser +import com.nextcloud.talk.models.json.conversations.Conversation +import com.nextcloud.talk.models.json.conversations.RoomOverall +import com.nextcloud.talk.models.json.participants.Participant +import com.nextcloud.talk.models.json.sharees.Sharee +import com.nextcloud.talk.newarch.domain.repository.offline.UsersRepository +import com.nextcloud.talk.newarch.local.models.UserNgEntity +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.ConductorRemapping +import com.nextcloud.talk.utils.KeyboardUtils +import com.nextcloud.talk.utils.bundle.BundleKeys +import com.uber.autodispose.AutoDispose +import eu.davidea.fastscroller.FastScroller +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.SelectableAdapter +import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import okhttp3.ResponseBody +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import org.koin.android.ext.android.inject +import org.parceler.Parcels +import java.util.ArrayList +import java.util.Collections +import java.util.HashMap +import java.util.HashSet +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class ContactsController : BaseController, + SearchView.OnQueryTextListener, + FlexibleAdapter.OnItemClickListener, + FastScroller.OnScrollStateChangeListener, + FlexibleAdapter.EndlessScrollListener { + + val usersRepository: UsersRepository by inject() + + @JvmField + @BindView(R.id.initial_relative_layout) + var initialRelativeLayout: RelativeLayout? = null + @JvmField + @BindView(R.id.secondary_relative_layout) + var secondaryRelativeLayout: RelativeLayout? = null + @JvmField + @BindView(R.id.progressBar) + var progressBar: ProgressBar? = null + @JvmField + @BindView(R.id.recyclerView) + var recyclerView: RecyclerView? = null + + @JvmField + @BindView(R.id.swipe_refresh_layout) + var swipeRefreshLayout: SwipeRefreshLayout? = null + + @JvmField + @BindView(R.id.fast_scroller) + var fastScroller: FastScroller? = null + + @JvmField + @BindView(R.id.call_header_layout) + var conversationPrivacyToogleLayout: RelativeLayout? = null + + @JvmField + @BindView(R.id.joinConversationViaLinkRelativeLayout) + var joinConversationViaLinkLayout: RelativeLayout? = null + + @JvmField + @BindView(R.id.generic_rv_layout) + var genericRvLayout: CoordinatorLayout? = null + + @JvmField + @Inject + var ncApi: NcApi? = null + private var credentials: String? = null + private var currentUser: UserNgEntity? = null + private var adapter: FlexibleAdapter>? = null + private var contactItems: MutableList>? = null + private var bottomSheet: BottomSheet? = null + private var bottomSheetView: View? = null + private var currentPage: Int = 0 + private var currentSearchPage: Int = 0 + + private var layoutManager: SmoothScrollLinearLayoutManager? = null + + private var searchItem: MenuItem? = null + private var searchView: SearchView? = null + + private var isNewConversationView: Boolean = false + private var isPublicCall: Boolean = false + + private var userHeaderItems = HashMap() + + private var alreadyFetching = false + private var canFetchFurther = true + private var canFetchSearchFurther = true + + private var doneMenuItem: MenuItem? = null + + private val selectedUserIds: MutableSet = mutableSetOf() + private val selectedGroupIds: MutableSet = mutableSetOf() + private var existingParticipants: List? = null + private var isAddingParticipantsView: Boolean = false + private var conversationToken: String? = null + + constructor() : super() { + setHasOptionsMenu(true) + } + + constructor(args: Bundle) : super() { + setHasOptionsMenu(true) + if (args.containsKey(BundleKeys.KEY_NEW_CONVERSATION)) { + isNewConversationView = true + existingParticipants = ArrayList() + } else if (args.containsKey(BundleKeys.KEY_ADD_PARTICIPANTS)) { + isAddingParticipantsView = true + conversationToken = args.getString(BundleKeys.KEY_TOKEN) + + existingParticipants = ArrayList() + + if (args.containsKey(BundleKeys.KEY_EXISTING_PARTICIPANTS)) { + existingParticipants = args.getStringArrayList(BundleKeys.KEY_EXISTING_PARTICIPANTS) + } + } + } + + override fun inflateView( + inflater: LayoutInflater, + container: ViewGroup + ): View { + return inflater.inflate(R.layout.controller_contacts_rv, container, false) + } + + override fun onDetach(view: View) { + eventBus!!.unregister(this) + super.onDetach(view) + } + + override fun onAttach(view: View) { + super.onAttach(view) + eventBus!!.register(this) + + if (isNewConversationView) { + toggleNewCallHeaderVisibility(!isPublicCall) + } + + if (isAddingParticipantsView) { + joinConversationViaLinkLayout!!.visibility = View.GONE + conversationPrivacyToogleLayout!!.visibility = View.GONE + } + } + + override fun onViewBound(view: View) { + super.onViewBound(view) + NextcloudTalkApplication.sharedApplication!! + .componentApplication + .inject(this) + + currentUser = usersRepository.getActiveUser() + + if (currentUser != null) { + credentials = ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token) + } + + if (adapter == null) { + contactItems = ArrayList() + adapter = FlexibleAdapter(contactItems, activity, true) + + if (currentUser != null) { + fetchData(true) + } + } + + setupAdapter() + prepareViews() + } + + private fun setupAdapter() { + adapter!!.setNotifyChangeOfUnfilteredItems(true) + .mode = SelectableAdapter.Mode.MULTI + + adapter!!.setEndlessScrollListener(this, ProgressItem()) + + adapter!!.setStickyHeaderElevation(5) + .setUnlinkAllItemsOnRemoveHeaders(true) + .setDisplayHeadersAtStartUp(true) + .setStickyHeaders(true) + + adapter!!.addListener(this) + } + + private fun selectionDone() { + if (!isAddingParticipantsView) { + if (!isPublicCall && selectedGroupIds.size + selectedUserIds.size == 1) { + val userId: String + var roomType = "1" + + if (selectedGroupIds.size == 1) { + roomType = "2" + userId = selectedGroupIds.iterator() + .next() + } else { + userId = selectedUserIds.iterator() + .next() + } + + val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom( + currentUser!!.baseUrl, roomType, + 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, currentUser) + bundle.putString( + BundleKeys.KEY_ROOM_TOKEN, + roomOverall.ocs.data.token + ) + bundle.putString( + BundleKeys.KEY_ROOM_ID, + roomOverall.ocs.data.conversationId + ) + + if (currentUser!!.hasSpreedFeatureCapability("chat-v2")) { + ncApi!!.getRoom( + credentials, + ApiUtils.getRoom( + currentUser!!.baseUrl, + roomOverall.ocs.data.token + ) + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .`as`(AutoDispose.autoDisposable(scopeProvider)) + .subscribe(object : Observer { + + override fun onSubscribe(d: Disposable) { + + } + + override fun onNext(roomOverall: RoomOverall) { + bundle.putParcelable( + BundleKeys.KEY_ACTIVE_CONVERSATION, + Parcels.wrap(roomOverall.ocs.data) + ) + + ConductorRemapping.remapChatController( + router, + currentUser!!.id, + roomOverall.ocs.data.token!!, bundle, true + ) + } + + override fun onError(e: Throwable) { + + } + + override fun onComplete() { + + } + }) + } else { + conversationIntent.putExtras(bundle) + startActivity(conversationIntent) + Handler().postDelayed({ + if (!isDestroyed && !isBeingDestroyed) { + router.popCurrentController() + } + }, 100) + } + } + + override fun onError(e: Throwable) { + + } + + override fun onComplete() {} + }) + } else { + + val bundle = Bundle() + val roomType: Conversation.ConversationType + if (isPublicCall) { + roomType = Conversation.ConversationType.PUBLIC_CONVERSATION + } else { + roomType = Conversation.ConversationType.GROUP_CONVERSATION + } + + val userIdsArray = ArrayList(selectedUserIds) + val groupIdsArray = ArrayList(selectedGroupIds) + + bundle.putParcelable( + BundleKeys.KEY_CONVERSATION_TYPE, + Parcels.wrap(roomType) + ) + bundle.putStringArrayList(BundleKeys.KEY_INVITED_PARTICIPANTS, userIdsArray) + bundle.putStringArrayList(BundleKeys.KEY_INVITED_GROUP, groupIdsArray) + bundle.putInt(BundleKeys.KEY_OPERATION_CODE, 11) + prepareAndShowBottomSheetWithBundle(bundle, true) + } + } else { + val userIdsArray = selectedUserIds.toTypedArray() + val groupIdsArray = selectedGroupIds.toTypedArray() + + val data = Data.Builder() + data.putLong(BundleKeys.KEY_INTERNAL_USER_ID, currentUser!!.id) + data.putString(BundleKeys.KEY_TOKEN, conversationToken) + data.putStringArray(BundleKeys.KEY_SELECTED_USERS, userIdsArray) + data.putStringArray(BundleKeys.KEY_SELECTED_GROUPS, groupIdsArray) + + val addParticipantsToConversationWorker = + OneTimeWorkRequest.Builder(AddParticipantsToConversation::class.java) + .setInputData( + data.build() + ) + .build() + WorkManager.getInstance() + .enqueue(addParticipantsToConversationWorker) + + router.popCurrentController() + } + } + + private fun initSearchView() { + if (activity != null) { + val searchManager = activity!!.getSystemService(Context.SEARCH_SERVICE) as SearchManager + if (searchItem != null) { + searchView = MenuItemCompat.getActionView(searchItem!!) as SearchView + searchView!!.maxWidth = Integer.MAX_VALUE + searchView!!.inputType = InputType.TYPE_TEXT_VARIATION_FILTER + var imeOptions = EditorInfo.IME_ACTION_DONE or EditorInfo.IME_FLAG_NO_FULLSCREEN + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && appPreferences!!.isKeyboardIncognito) { + imeOptions = imeOptions or EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING + } + searchView!!.imeOptions = imeOptions + searchView!!.queryHint = resources!!.getString(R.string.nc_search) + if (searchManager != null) { + searchView!!.setSearchableInfo( + searchManager.getSearchableInfo(activity!!.componentName) + ) + } + searchView!!.setOnQueryTextListener(this) + } + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + router.popCurrentController() + return true + } + R.id.contacts_selection_done -> { + selectionDone() + return true + } + else -> return super.onOptionsItemSelected(item) + } + } + + override fun onCreateOptionsMenu( + menu: Menu, + inflater: MenuInflater + ) { + super.onCreateOptionsMenu(menu, inflater) + inflater.inflate(R.menu.menu_contacts, menu) + searchItem = menu.findItem(R.id.action_search) + doneMenuItem = menu.findItem(R.id.contacts_selection_done) + + initSearchView() + } + + override fun onPrepareOptionsMenu(menu: Menu) { + super.onPrepareOptionsMenu(menu) + checkAndHandleDoneMenuItem() + if (adapter!!.hasFilter()) { + searchItem!!.expandActionView() + searchView!!.setQuery(adapter!!.getFilter(String::class.java) as CharSequence?, false) + } + } + + private fun fetchData(startFromScratch: Boolean) { + alreadyFetching = true + val shareeHashSet = HashSet() + val autocompleteUsersHashSet = HashSet() + + userHeaderItems = HashMap() + + val query = adapter!!.getFilter(String::class.java) + + val retrofitBucket: RetrofitBucket + var serverIs14OrUp = false + if (currentUser!!.hasSpreedFeatureCapability("last-room-activity")) { + // a hack to see if we're on 14 or not + retrofitBucket = + ApiUtils.getRetrofitBucketForContactsSearchFor14(currentUser!!.baseUrl, query) + serverIs14OrUp = true + } else { + retrofitBucket = ApiUtils.getRetrofitBucketForContactsSearch(currentUser!!.baseUrl, query) + } + + var page = 1 + if (!startFromScratch) { + if (TextUtils.isEmpty(query)) { + page = currentPage + 1 + } else { + page = currentSearchPage + 1 + } + } + + val modifiedQueryMap = HashMap(retrofitBucket.queryMap) + modifiedQueryMap["page"] = page + modifiedQueryMap["perPage"] = 100 + + var shareTypesList: MutableList? = null + + if (serverIs14OrUp) { + shareTypesList = ArrayList() + // users + shareTypesList.add("0") + // groups + shareTypesList.add("1") + // mails + //shareTypesList.add("4"); + + modifiedQueryMap["shareTypes[]"] = shareTypesList + } + + val finalServerIs14OrUp = serverIs14OrUp + ncApi!!.getContactsWithSearchParam( + credentials, + retrofitBucket.url, shareTypesList, modifiedQueryMap + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .retry(3) + .`as`(AutoDispose.autoDisposable(scopeProvider)) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) {} + + override fun onNext(responseBody: ResponseBody) { + var participant: Participant + + val newUserItemList = ArrayList>() + + try { + val autocompleteOverall = + LoganSquare.parse(responseBody.string(), AutocompleteOverall::class.java) + autocompleteUsersHashSet.addAll(autocompleteOverall.ocs.data) + + for (autocompleteUser in autocompleteUsersHashSet) { + if (autocompleteUser.id != currentUser!!.userId && !existingParticipants!!.contains( + autocompleteUser.id + ) + ) { + participant = Participant() + participant.userId = autocompleteUser.id + participant.displayName = autocompleteUser.label + participant.source = autocompleteUser.source + + val headerTitle: String + + if (autocompleteUser.source != "groups") { + headerTitle = participant.displayName + .substring(0, 1) + .toUpperCase() + } else { + headerTitle = resources!!.getString(R.string.nc_groups) + } + + val genericTextHeaderItem: GenericTextHeaderItem + if (!userHeaderItems.containsKey(headerTitle)) { + genericTextHeaderItem = GenericTextHeaderItem(headerTitle) + userHeaderItems[headerTitle] = genericTextHeaderItem + } + + val newContactItem = UserItem( + participant, currentUser!!, + userHeaderItems[headerTitle], activity!! + ) + + if (!contactItems!!.contains(newContactItem)) { + newUserItemList.add(newContactItem) + } + } + } + } catch (exception: Exception) { + Log.e(TAG, "Parsing response body failed while getting contacts") + } + + if (TextUtils.isEmpty(modifiedQueryMap["search"] as CharSequence?)) { + canFetchFurther = + !shareeHashSet.isEmpty() || finalServerIs14OrUp && autocompleteUsersHashSet.size == 100 + currentPage = modifiedQueryMap["page"] as Int + } else { + canFetchSearchFurther = + !shareeHashSet.isEmpty() || finalServerIs14OrUp && autocompleteUsersHashSet.size == 100 + currentSearchPage = modifiedQueryMap["page"] as Int + } + + userHeaderItems = HashMap() + contactItems!!.addAll(newUserItemList) + + newUserItemList.sortWith(Comparator { o1, o2 -> + val firstName: String + val secondName: String + + if (o1 is UserItem) { + firstName = o1.model.displayName + } else { + firstName = (o1 as GenericTextHeaderItem).model + } + + if (o2 is UserItem) { + secondName = o2.model.displayName + } else { + secondName = (o2 as GenericTextHeaderItem).model + } + + if (o1 is UserItem && o2 is UserItem) { + if ("groups" == o1.model.source && "groups" == o2.model.source) { + firstName.compareTo(secondName, ignoreCase = true) + } else if ("groups" == o1.model.source) { + -1 + } else if ("groups" == o2.model.source) { + 1 + } + } + + firstName.compareTo(secondName, ignoreCase = true) + }) + + contactItems!!.sortWith(Comparator { o1, o2 -> + val firstName: String + val secondName: String + + if (o1 is UserItem) { + firstName = o1.model.displayName + } else { + firstName = (o1 as GenericTextHeaderItem).model + } + + if (o2 is UserItem) { + secondName = o2.model.displayName + } else { + secondName = (o2 as GenericTextHeaderItem).model + } + + if (o1 is UserItem && o2 is UserItem) { + if ("groups" == o1.model.source && "groups" == o2.model.source) { + firstName.compareTo(secondName, ignoreCase = true) + } else if ("groups" == o1.model.source) { + -1 + } else if ("groups" == o2.model.source) { + 1 + } + } + + firstName.compareTo(secondName, ignoreCase = true) + }) + + if (newUserItemList.size > 0) { + adapter!!.updateDataSet(newUserItemList) + } else { + adapter!!.filterItems() + } + + if (swipeRefreshLayout != null) { + swipeRefreshLayout!!.isRefreshing = false + } + } + + override fun onError(e: Throwable) { + if (swipeRefreshLayout != null) { + swipeRefreshLayout!!.isRefreshing = false + } + } + + override fun onComplete() { + if (swipeRefreshLayout != null) { + swipeRefreshLayout!!.isRefreshing = false + } + alreadyFetching = false + + disengageProgressBar() + } + }) + } + + private fun prepareViews() { + layoutManager = SmoothScrollLinearLayoutManager(activity!!) + recyclerView!!.layoutManager = layoutManager + recyclerView!!.setHasFixedSize(true) + recyclerView!!.adapter = adapter + + swipeRefreshLayout!!.setOnRefreshListener { fetchData(true) } + swipeRefreshLayout!!.setColorSchemeResources(R.color.colorPrimary) + + fastScroller!!.addOnScrollStateChangeListener(this) + adapter!!.setFastScroller(fastScroller) + fastScroller!!.setBubbleTextCreator { position -> + val abstractFlexibleItem = adapter!!.getItem(position) + if (abstractFlexibleItem is UserItem) { + (adapter!!.getItem(position) as UserItem).header!!.model + } else if (abstractFlexibleItem is GenericTextHeaderItem) { + (adapter!!.getItem(position) as GenericTextHeaderItem).model + } else { + "" + } + } + + disengageProgressBar() + } + + private fun disengageProgressBar() { + if (!alreadyFetching) { + progressBar!!.visibility = View.GONE + genericRvLayout!!.visibility = View.VISIBLE + + if (isNewConversationView) { + conversationPrivacyToogleLayout!!.visibility = View.VISIBLE + joinConversationViaLinkLayout!!.visibility = View.VISIBLE + } + } + } + + public override fun onSaveViewState( + view: View, + outState: Bundle + ) { + adapter!!.onSaveInstanceState(outState) + super.onSaveViewState(view, outState) + } + + public override fun onRestoreViewState( + view: View, + savedViewState: Bundle + ) { + super.onRestoreViewState(view, savedViewState) + if (adapter != null) { + adapter!!.onRestoreInstanceState(savedViewState) + } + } + + override fun onQueryTextChange(newText: String): Boolean { + if (newText != "" && adapter!!.hasNewFilter(newText)) { + adapter!!.setFilter(newText) + fetchData(true) + } else if (newText == "") { + adapter!!.setFilter("") + adapter!!.updateDataSet(contactItems) + } + + if (swipeRefreshLayout != null) { + swipeRefreshLayout!!.isEnabled = !adapter!!.hasFilter() + } + + return true + } + + override fun onQueryTextSubmit(query: String): Boolean { + return onQueryTextChange(query) + } + + private fun checkAndHandleDoneMenuItem() { + if (adapter != null && doneMenuItem != null) { + if (selectedGroupIds.size + selectedUserIds.size > 0 || isPublicCall) { + doneMenuItem!!.isVisible = true + } else { + doneMenuItem!!.isVisible = false + } + } else if (doneMenuItem != null) { + doneMenuItem!!.isVisible = false + } + } + + override fun getTitle(): String? { + return if (!isNewConversationView && !isAddingParticipantsView) { + resources!!.getString(R.string.nc_app_name) + } else { + resources!!.getString(R.string.nc_select_contacts) + } + } + + override fun onFastScrollerStateChange(scrolling: Boolean) { + swipeRefreshLayout!!.isEnabled = !scrolling + } + + private fun prepareAndShowBottomSheetWithBundle( + bundle: Bundle, + showEntrySheet: Boolean + ) { + if (bottomSheetView == null) { + bottomSheetView = activity!!.layoutInflater.inflate(R.layout.bottom_sheet, null, false) + } + + if (bottomSheet == null) { + bottomSheet = BottomSheet.Builder(activity!!) + .setView(bottomSheetView) + .create() + } + + if (showEntrySheet) { + getChildRouter((bottomSheetView as ViewGroup?)!!).setRoot( + RouterTransaction.with(EntryMenuController(bundle)) + .popChangeHandler(VerticalChangeHandler()) + .pushChangeHandler(VerticalChangeHandler()) + ) + } else { + getChildRouter((bottomSheetView as ViewGroup?)!!).setRoot( + RouterTransaction.with(OperationsMenuController(bundle)) + .popChangeHandler(VerticalChangeHandler()) + .pushChangeHandler(VerticalChangeHandler()) + ) + } + + bottomSheet!!.setOnShowListener { dialog -> + if (showEntrySheet) { + KeyboardUtils(activity!!, bottomSheet!!.layout, true) + } else { + eventBus.post( + BottomSheetLockEvent( + false, 0, + false, false + ) + ) + } + } + + bottomSheet!!.setOnDismissListener { dialog -> + actionBar!!.setDisplayHomeAsUpEnabled( + router.backstackSize > 1 + ) + } + + bottomSheet!!.show() + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onMessageEvent(bottomSheetLockEvent: BottomSheetLockEvent) { + + if (bottomSheet != null) { + if (!bottomSheetLockEvent.cancelable) { + bottomSheet!!.setCancelable(bottomSheetLockEvent.cancelable) + } else { + bottomSheet!!.setCancelable(bottomSheetLockEvent.cancelable) + if (bottomSheet!!.isShowing && bottomSheetLockEvent.cancel) { + Handler().postDelayed({ + bottomSheet!!.setOnCancelListener(null) + bottomSheet!!.cancel() + }, bottomSheetLockEvent.delay.toLong()) + } + } + } + } + + override fun onItemClick( + view: View, + position: Int + ): Boolean { + if (adapter!!.getItem(position) is UserItem) { + if (!isNewConversationView && !isAddingParticipantsView) { + val userItem = adapter!!.getItem(position) as UserItem? + var roomType = "1" + + if ("groups" == userItem!!.model.source) { + roomType = "2" + } + + val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom( + currentUser!!.baseUrl, roomType, + userItem.model.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) { + if (activity != null) { + val conversationIntent = Intent(activity, MagicCallActivity::class.java) + val bundle = Bundle() + bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, currentUser) + bundle.putString( + BundleKeys.KEY_ROOM_TOKEN, + roomOverall.ocs.data.token + ) + bundle.putString( + BundleKeys.KEY_ROOM_ID, + roomOverall.ocs.data.conversationId + ) + conversationIntent.putExtras(bundle) + + if (currentUser!!.hasSpreedFeatureCapability("chat-v2")) { + bundle.putParcelable( + BundleKeys.KEY_ACTIVE_CONVERSATION, + Parcels.wrap(roomOverall.ocs.data) + ) + + ConductorRemapping.remapChatController( + router, + currentUser!!.id, + roomOverall.ocs.data.token!!, bundle, true + ) + } else { + startActivity(conversationIntent) + Handler().postDelayed({ router.popCurrentController() }, 100) + } + } + } + + override fun onError(e: Throwable) { + + } + + override fun onComplete() { + + } + }) + } else { + val participant = (adapter!!.getItem(position) as UserItem).model + participant.selected = !participant.selected + + if ("groups" == participant.source) { + if (participant.selected) { + selectedGroupIds.add(participant.userId) + } else { + selectedGroupIds.remove(participant.userId) + } + } else { + if (participant.selected) { + selectedUserIds.add(participant.userId) + } else { + selectedUserIds.remove(participant.userId) + } + } + + if (currentUser!!.hasSpreedFeatureCapability("last-room-activity") + && !currentUser!!.hasSpreedFeatureCapability("invite-groups-and-mails") && + "groups" == (adapter!!.getItem(position) as UserItem).model.source && + participant.selected && + adapter!!.selectedItemCount > 1 + ) { + val currentItems = adapter!!.currentItems + var internalParticipant: Participant + for (i in currentItems.indices) { + if (currentItems[i] is UserItem) { + internalParticipant = (currentItems[i] as UserItem).model + if (internalParticipant.userId == participant.userId + && + "groups" == internalParticipant.source + && internalParticipant.selected + ) { + internalParticipant.selected = false + selectedGroupIds.remove(internalParticipant.userId) + } + } + } + } + + adapter!!.notifyDataSetChanged() + checkAndHandleDoneMenuItem() + } + } + return true + } + + @Optional + @OnClick(R.id.joinConversationViaLinkRelativeLayout) + internal fun joinConversationViaLink() { + val bundle = Bundle() + bundle.putInt(BundleKeys.KEY_OPERATION_CODE, 10) + + prepareAndShowBottomSheetWithBundle(bundle, true) + } + + @Optional + @OnClick(R.id.call_header_layout) + internal fun toggleCallHeader() { + toggleNewCallHeaderVisibility(isPublicCall) + isPublicCall = !isPublicCall + + if (isPublicCall) { + joinConversationViaLinkLayout!!.visibility = View.GONE + } else { + joinConversationViaLinkLayout!!.visibility = View.VISIBLE + } + + if (isPublicCall) { + val currentItems = adapter!!.currentItems + var internalParticipant: Participant + for (i in currentItems.indices) { + if (currentItems[i] is UserItem) { + internalParticipant = (currentItems[i] as UserItem).model + if ("groups" == internalParticipant.source && internalParticipant.selected) { + internalParticipant.selected = false + selectedGroupIds.remove(internalParticipant.userId) + } + } + } + } + + for (i in 0 until adapter!!.itemCount) { + if (adapter!!.getItem(i) is UserItem) { + val userItem = adapter!!.getItem(i) as UserItem? + if ("groups" == userItem!!.model.source) { + userItem.isEnabled = !isPublicCall + } + } + } + + checkAndHandleDoneMenuItem() + adapter!!.notifyDataSetChanged() + } + + private fun toggleNewCallHeaderVisibility(showInitialLayout: Boolean) { + if (showInitialLayout) { + initialRelativeLayout!!.visibility = View.VISIBLE + secondaryRelativeLayout!!.visibility = View.GONE + } else { + initialRelativeLayout!!.visibility = View.GONE + secondaryRelativeLayout!!.visibility = View.VISIBLE + } + } + + override fun noMoreLoad(newItemsSize: Int) {} + + override fun onLoadMore( + lastPosition: Int, + currentPage: Int + ) { + + if (!alreadyFetching && (searchView != null && searchView!!.isIconified && canFetchFurther || !TextUtils.isEmpty( + adapter!!.getFilter(String::class.java) + ) && canFetchSearchFurther) + ) { + fetchData(false) + } else { + adapter!!.onLoadMoreComplete(null) + } + } + + companion object { + + val TAG = "ContactsController" + } +} 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 7a2b8c00c..66dfdc542 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/ConversationInfoController.kt @@ -73,6 +73,9 @@ import com.nextcloud.talk.models.json.converters.EnumNotificationLevelConverter import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.models.json.participants.Participant import com.nextcloud.talk.models.json.participants.ParticipantsOverall +import com.nextcloud.talk.newarch.local.models.UserNgEntity +import com.nextcloud.talk.newarch.local.models.getCredentials +import com.nextcloud.talk.newarch.local.models.hasSpreedFeatureCapability import com.nextcloud.talk.newarch.utils.getCredentials import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.DateUtils @@ -174,7 +177,7 @@ class ConversationInfoController(args: Bundle) : BaseController(), lateinit var userUtils: UserUtils private val conversationToken: String? - private val conversationUser: UserEntity? + private val conversationUser: UserNgEntity? private val credentials: String? private var roomDisposable: Disposable? = null private var participantsDisposable: Disposable? = null @@ -245,7 +248,7 @@ class ConversationInfoController(args: Bundle) : BaseController(), if (databaseStorageModule == null) { databaseStorageModule = DatabaseStorageModule( - conversationUser, conversationToken, this) + conversationUser!!, conversationToken!!, this) } notificationsPreferenceScreen.setStorageModule(databaseStorageModule) diff --git a/app/src/main/java/com/nextcloud/talk/events/BottomSheetLockEvent.java b/app/src/main/java/com/nextcloud/talk/events/BottomSheetLockEvent.java index c1ef8bbd1..4995ca144 100644 --- a/app/src/main/java/com/nextcloud/talk/events/BottomSheetLockEvent.java +++ b/app/src/main/java/com/nextcloud/talk/events/BottomSheetLockEvent.java @@ -24,11 +24,11 @@ import lombok.Data; @Data public class BottomSheetLockEvent { - private final boolean cancelable; - private final int delay; - private final boolean shouldRefreshData; - private final boolean cancel; - private boolean dismissView; + public final boolean cancelable; + public final int delay; + public final boolean shouldRefreshData; + public final boolean cancel; + public boolean dismissView; public BottomSheetLockEvent(boolean cancelable, int delay, boolean shouldRefreshData, boolean cancel) { diff --git a/app/src/main/java/com/nextcloud/talk/events/MediaStreamEvent.java b/app/src/main/java/com/nextcloud/talk/events/MediaStreamEvent.java index e6c75f4a8..4dded2a7f 100644 --- a/app/src/main/java/com/nextcloud/talk/events/MediaStreamEvent.java +++ b/app/src/main/java/com/nextcloud/talk/events/MediaStreamEvent.java @@ -26,9 +26,9 @@ import org.webrtc.MediaStream; @Data public class MediaStreamEvent { - private final MediaStream mediaStream; - private final String session; - private final String videoStreamType; + public final MediaStream mediaStream; + public final String session; + public final String videoStreamType; public MediaStreamEvent(@Nullable MediaStream mediaStream, String session, String videoStreamType) { diff --git a/app/src/main/java/com/nextcloud/talk/events/NetworkEvent.java b/app/src/main/java/com/nextcloud/talk/events/NetworkEvent.java index 46fa1da3e..a6ad6ed33 100644 --- a/app/src/main/java/com/nextcloud/talk/events/NetworkEvent.java +++ b/app/src/main/java/com/nextcloud/talk/events/NetworkEvent.java @@ -24,7 +24,7 @@ import lombok.Data; @Data public class NetworkEvent { - private final NetworkConnectionEvent networkConnectionEvent; + public final NetworkConnectionEvent networkConnectionEvent; public NetworkEvent(NetworkConnectionEvent networkConnectionEvent) { this.networkConnectionEvent = networkConnectionEvent; diff --git a/app/src/main/java/com/nextcloud/talk/events/PeerConnectionEvent.java b/app/src/main/java/com/nextcloud/talk/events/PeerConnectionEvent.java index 459a9338f..84ed186de 100644 --- a/app/src/main/java/com/nextcloud/talk/events/PeerConnectionEvent.java +++ b/app/src/main/java/com/nextcloud/talk/events/PeerConnectionEvent.java @@ -25,11 +25,11 @@ import lombok.Data; @Data public class PeerConnectionEvent { - private final PeerConnectionEventType peerConnectionEventType; - private final String sessionId; - private final String nick; - private final Boolean changeValue; - private final String videoStreamType; + public final PeerConnectionEventType peerConnectionEventType; + public final String sessionId; + public final String nick; + public final Boolean changeValue; + public final String videoStreamType; public PeerConnectionEvent(PeerConnectionEventType peerConnectionEventType, @Nullable String sessionId, diff --git a/app/src/main/java/com/nextcloud/talk/events/SessionDescriptionSendEvent.java b/app/src/main/java/com/nextcloud/talk/events/SessionDescriptionSendEvent.java index 1e8290c89..2c161722f 100644 --- a/app/src/main/java/com/nextcloud/talk/events/SessionDescriptionSendEvent.java +++ b/app/src/main/java/com/nextcloud/talk/events/SessionDescriptionSendEvent.java @@ -28,12 +28,12 @@ import org.webrtc.SessionDescription; @Data public class SessionDescriptionSendEvent { @Nullable - private final SessionDescription sessionDescription; - private final String peerId; - private final String type; + public final SessionDescription sessionDescription; + public final String peerId; + public final String type; @Nullable - private final NCIceCandidate ncIceCandidate; - private final String videoStreamType; + public final NCIceCandidate ncIceCandidate; + public final String videoStreamType; public SessionDescriptionSendEvent(@Nullable SessionDescription sessionDescription, String peerId, String type, diff --git a/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt index 9838997da..7434d24c6 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt +++ b/app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.kt @@ -76,6 +76,10 @@ import com.nextcloud.talk.models.json.conversations.RoomOverall import com.nextcloud.talk.models.json.notifications.NotificationOverall import com.nextcloud.talk.models.json.push.DecryptedPushMessage import com.nextcloud.talk.models.json.push.NotificationUser +import com.nextcloud.talk.newarch.domain.repository.offline.UsersRepository +import com.nextcloud.talk.newarch.local.models.UserNgEntity +import com.nextcloud.talk.newarch.local.models.getCredentials +import com.nextcloud.talk.newarch.local.models.hasSpreedFeatureCapability import com.nextcloud.talk.newarch.utils.Images import com.nextcloud.talk.newarch.utils.getCredentials import com.nextcloud.talk.utils.ApiUtils @@ -101,11 +105,12 @@ import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY import com.nextcloud.talk.utils.database.arbitrarystorage.ArbitraryStorageUtils import com.nextcloud.talk.utils.preferences.AppPreferences -import com.nextcloud.talk.utils.singletons.ApplicationWideCurrentRoomHolder import io.reactivex.Observer import io.reactivex.disposables.Disposable import okhttp3.JavaNetCookieJar import okhttp3.OkHttpClient +import org.koin.core.KoinComponent +import org.koin.core.inject import org.parceler.Parcels import retrofit2.Retrofit import java.io.IOException @@ -124,19 +129,16 @@ import javax.inject.Inject class NotificationWorker( context: Context, workerParams: WorkerParameters -) : Worker(context, workerParams) { - @JvmField - @Inject - var appPreferences: AppPreferences? = null +) : Worker(context, workerParams), KoinComponent { + + val appPreferences: AppPreferences by inject() + val retrofit: Retrofit by inject() + val okHttpClient: OkHttpClient by inject() + val usersRepository: UsersRepository by inject() + @JvmField @Inject var arbitraryStorageUtils: ArbitraryStorageUtils? = null - @JvmField - @Inject - var retrofit: Retrofit? = null - @JvmField - @Inject - var okHttpClient: OkHttpClient? = null private var ncApi: NcApi? = null private var decryptedPushMessage: DecryptedPushMessage? = null private var context: Context? = null @@ -148,7 +150,7 @@ class NotificationWorker( private fun showNotificationForCallWithNoPing(intent: Intent) { - val userEntity: UserEntity = + val userEntity: UserNgEntity = signatureVerification!!.userEntity var arbitraryStorageEntity: ArbitraryStorageEntity? @@ -224,7 +226,7 @@ class NotificationWorker( } private fun showNotificationWithObjectData(intent: Intent) { - val userEntity: UserEntity = + val userEntity: UserNgEntity = signatureVerification!!.userEntity ncApi!!.getNotification( credentials, ApiUtils.getUrlForNotificationWithId( @@ -575,8 +577,7 @@ class NotificationWorker( } } - if (soundUri != null && !ApplicationWideCurrentRoomHolder.getInstance().isInCall && - (shouldPlaySound(importantConversation)) + if (soundUri != null && (shouldPlaySound(importantConversation)) ) { val audioAttributesBuilder: AudioAttributes.Builder = AudioAttributes.Builder() @@ -630,7 +631,7 @@ class NotificationWorker( data.getString(KEY_NOTIFICATION_SIGNATURE) val base64DecodedSubject: ByteArray = Base64.decode(subject, Base64.DEFAULT) val base64DecodedSignature: ByteArray = Base64.decode(signature, Base64.DEFAULT) - val pushUtils = PushUtils() + val pushUtils = PushUtils(usersRepository) val privateKey = pushUtils.readKeyFromFile(false) as PrivateKey try { signatureVerification = pushUtils.verifySignature( diff --git a/app/src/main/java/com/nextcloud/talk/jobs/PushRegistrationWorker.java b/app/src/main/java/com/nextcloud/talk/jobs/PushRegistrationWorker.java deleted file mode 100644 index 8a1be0c8b..000000000 --- a/app/src/main/java/com/nextcloud/talk/jobs/PushRegistrationWorker.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Nextcloud Talk application - * - * @author Mario Danic - * Copyright (C) 2017 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.jobs; - -import android.content.Context; -import androidx.annotation.NonNull; -import androidx.work.Worker; -import androidx.work.WorkerParameters; -import com.nextcloud.talk.utils.PushUtils; - -public class PushRegistrationWorker extends Worker { - public static final String TAG = "PushRegistrationWorker"; - - public PushRegistrationWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { - super(context, workerParams); - } - - @NonNull - @Override - public Result doWork() { - PushUtils pushUtils = new PushUtils(); - pushUtils.generateRsa2048KeyPair(); - pushUtils.pushRegistrationToServer(); - - return Result.success(); - } -} diff --git a/app/src/main/java/com/nextcloud/talk/jobs/PushRegistrationWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/PushRegistrationWorker.kt new file mode 100644 index 000000000..d28aeda5f --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/jobs/PushRegistrationWorker.kt @@ -0,0 +1,49 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * Copyright (C) 2017 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.jobs + +import android.content.Context +import androidx.work.ListenableWorker.Result +import androidx.work.Worker +import androidx.work.WorkerParameters +import com.nextcloud.talk.newarch.domain.repository.offline.UsersRepository +import com.nextcloud.talk.utils.PushUtils +import org.koin.core.KoinComponent +import org.koin.core.inject + +class PushRegistrationWorker( + context: Context, + workerParams: WorkerParameters +) : Worker(context, workerParams), KoinComponent { + + val usersRepository: UsersRepository by inject() + + override fun doWork(): Result { + val pushUtils = PushUtils(usersRepository) + pushUtils.generateRsa2048KeyPair() + pushUtils.pushRegistrationToServer() + return Result.success() + } + + companion object { + const val TAG = "PushRegistrationWorker" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/jobs/WebsocketConnectionsWorker.java b/app/src/main/java/com/nextcloud/talk/jobs/WebsocketConnectionsWorker.java deleted file mode 100644 index adfdfab05..000000000 --- a/app/src/main/java/com/nextcloud/talk/jobs/WebsocketConnectionsWorker.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Nextcloud Talk application - * - * @author Mario Danic - * Copyright (C) 2017-2018 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.jobs; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.text.TextUtils; -import android.util.Log; -import androidx.annotation.NonNull; -import androidx.work.Worker; -import androidx.work.WorkerParameters; -import autodagger.AutoInjector; -import com.bluelinelabs.logansquare.LoganSquare; -import com.nextcloud.talk.application.NextcloudTalkApplication; -import com.nextcloud.talk.models.ExternalSignalingServer; -import com.nextcloud.talk.models.database.UserEntity; -import com.nextcloud.talk.utils.database.user.UserUtils; -import com.nextcloud.talk.webrtc.WebSocketConnectionHelper; -import java.io.IOException; -import java.util.List; -import javax.inject.Inject; - -@AutoInjector(NextcloudTalkApplication.class) -public class WebsocketConnectionsWorker extends Worker { - - private static final String TAG = "WebsocketConnectionsWorker"; - - @Inject - UserUtils userUtils; - - public WebsocketConnectionsWorker(@NonNull Context context, - @NonNull WorkerParameters workerParams) { - super(context, workerParams); - } - - @SuppressLint("LongLogTag") - @NonNull - @Override - public Result doWork() { - NextcloudTalkApplication.Companion.getSharedApplication() - .getComponentApplication() - .inject(this); - - List userEntityList = userUtils.getUsers(); - UserEntity userEntity; - ExternalSignalingServer externalSignalingServer; - WebSocketConnectionHelper webSocketConnectionHelper = new WebSocketConnectionHelper(); - for (int i = 0; i < userEntityList.size(); i++) { - userEntity = userEntityList.get(i); - if (!TextUtils.isEmpty(userEntity.getExternalSignalingServer())) { - try { - externalSignalingServer = LoganSquare.parse(userEntity.getExternalSignalingServer(), - ExternalSignalingServer.class); - if (!TextUtils.isEmpty(externalSignalingServer.getExternalSignalingServer()) && - !TextUtils.isEmpty(externalSignalingServer.getExternalSignalingTicket())) { - WebSocketConnectionHelper.getExternalSignalingInstanceForServer( - externalSignalingServer.getExternalSignalingServer(), - userEntity, externalSignalingServer.getExternalSignalingTicket(), - false); - } - } catch (IOException e) { - Log.e(TAG, "Failed to parse external signaling server"); - } - } - } - - return Result.success(); - } -} diff --git a/app/src/main/java/com/nextcloud/talk/jobs/WebsocketConnectionsWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/WebsocketConnectionsWorker.kt new file mode 100644 index 000000000..5a0f372a1 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/jobs/WebsocketConnectionsWorker.kt @@ -0,0 +1,80 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * Copyright (C) 2017-2018 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.jobs + +import android.annotation.SuppressLint +import android.content.Context +import android.text.TextUtils +import android.util.Log +import androidx.work.ListenableWorker +import androidx.work.Worker +import androidx.work.WorkerParameters +import autodagger.AutoInjector +import com.bluelinelabs.logansquare.LoganSquare +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.models.ExternalSignalingServer +import com.nextcloud.talk.models.database.UserEntity +import com.nextcloud.talk.newarch.domain.repository.offline.UsersRepository +import com.nextcloud.talk.newarch.local.models.UserNgEntity +import com.nextcloud.talk.utils.database.user.UserUtils +import com.nextcloud.talk.webrtc.WebSocketConnectionHelper +import org.koin.core.KoinComponent +import org.koin.core.inject +import java.io.IOException +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class WebsocketConnectionsWorker( + context: Context, + workerParams: WorkerParameters +) : Worker(context, workerParams), KoinComponent { + + val usersRepository: UsersRepository by inject() + + override fun doWork(): Result { + NextcloudTalkApplication.sharedApplication!! + .componentApplication + .inject(this) + + val userEntityList = usersRepository.getUsers() + var userEntity: UserNgEntity + for (i in userEntityList.indices) { + userEntity = userEntityList[i] + if (userEntity.externalSignaling != null) { + if (!userEntity.externalSignaling!!.externalSignalingServer.isNullOrEmpty() && + !userEntity.externalSignaling!!.externalSignalingTicket.isNullOrEmpty()) { + WebSocketConnectionHelper.getExternalSignalingInstanceForServer( + userEntity.externalSignaling!!.externalSignalingServer, + userEntity, userEntity.externalSignaling!!.externalSignalingTicket, + false + ) + } + + } + } + + return Result.success() + } + + companion object { + private val TAG = "WebsocketConnectionsWorker" + } +} diff --git a/app/src/main/java/com/nextcloud/talk/models/ExternalSignalingServer.java b/app/src/main/java/com/nextcloud/talk/models/ExternalSignalingServer.kt similarity index 60% rename from app/src/main/java/com/nextcloud/talk/models/ExternalSignalingServer.java rename to app/src/main/java/com/nextcloud/talk/models/ExternalSignalingServer.kt index 095a21827..e484a1cca 100644 --- a/app/src/main/java/com/nextcloud/talk/models/ExternalSignalingServer.java +++ b/app/src/main/java/com/nextcloud/talk/models/ExternalSignalingServer.kt @@ -18,19 +18,22 @@ * along with this program. If not, see . */ -package com.nextcloud.talk.models; +package com.nextcloud.talk.models -import com.bluelinelabs.logansquare.annotation.JsonField; -import com.bluelinelabs.logansquare.annotation.JsonObject; -import lombok.Data; -import org.parceler.Parcel; +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.android.parcel.Parcelize +import lombok.Data +import org.parceler.Parcel @Data @Parcel @JsonObject -public class ExternalSignalingServer { - @JsonField(name = "externalSignalingServer") - public String externalSignalingServer; - @JsonField(name = "externalSignalingTicket") - public String externalSignalingTicket; -} +@Parcelize +data class ExternalSignalingServer( + @JsonField(name = ["externalSignalingServer"]) + var externalSignalingServer: String? = null, + @JsonField(name = ["externalSignalingTicket"]) + var externalSignalingTicket: String? = null +): Parcelable diff --git a/app/src/main/java/com/nextcloud/talk/models/SignatureVerification.java b/app/src/main/java/com/nextcloud/talk/models/SignatureVerification.java index d0e59fc9f..bf5e56312 100644 --- a/app/src/main/java/com/nextcloud/talk/models/SignatureVerification.java +++ b/app/src/main/java/com/nextcloud/talk/models/SignatureVerification.java @@ -21,6 +21,7 @@ package com.nextcloud.talk.models; import com.nextcloud.talk.models.database.UserEntity; +import com.nextcloud.talk.newarch.local.models.UserNgEntity; import lombok.Data; import org.parceler.Parcel; @@ -28,5 +29,5 @@ import org.parceler.Parcel; @Parcel public class SignatureVerification { public boolean signatureValid; - public UserEntity userEntity; + public UserNgEntity userEntity; } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/autocomplete/AutocompleteOCS.java b/app/src/main/java/com/nextcloud/talk/models/json/autocomplete/AutocompleteOCS.java index 19c7e1d89..29bd1e986 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/autocomplete/AutocompleteOCS.java +++ b/app/src/main/java/com/nextcloud/talk/models/json/autocomplete/AutocompleteOCS.java @@ -32,5 +32,5 @@ import org.parceler.Parcel; @JsonObject public class AutocompleteOCS extends GenericOCS { @JsonField(name = "data") - List data; + public List data; } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/autocomplete/AutocompleteOverall.java b/app/src/main/java/com/nextcloud/talk/models/json/autocomplete/AutocompleteOverall.java index 5e7b301bf..4ffcb37a6 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/autocomplete/AutocompleteOverall.java +++ b/app/src/main/java/com/nextcloud/talk/models/json/autocomplete/AutocompleteOverall.java @@ -30,5 +30,5 @@ import org.parceler.Parcel; @JsonObject public class AutocompleteOverall { @JsonField(name = "ocs") - AutocompleteOCS ocs; + public AutocompleteOCS ocs; } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/autocomplete/AutocompleteUser.java b/app/src/main/java/com/nextcloud/talk/models/json/autocomplete/AutocompleteUser.java index 9ad897c02..a5a7c26f4 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/autocomplete/AutocompleteUser.java +++ b/app/src/main/java/com/nextcloud/talk/models/json/autocomplete/AutocompleteUser.java @@ -30,11 +30,11 @@ import org.parceler.Parcel; @JsonObject public class AutocompleteUser { @JsonField(name = "id") - String id; + public String id; @JsonField(name = "label") - String label; + public String label; @JsonField(name = "source") - String source; + public String source; } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/CapabilitiesList.java b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/CapabilitiesList.java index 9be731be0..b5b89d9ec 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/CapabilitiesList.java +++ b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/CapabilitiesList.java @@ -30,5 +30,5 @@ import org.parceler.Parcel; @JsonObject public class CapabilitiesList { @JsonField(name = "capabilities") - Capabilities capabilities; + public Capabilities capabilities; } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/CapabilitiesOCS.java b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/CapabilitiesOCS.java index ebe422f25..f793bf596 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/CapabilitiesOCS.java +++ b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/CapabilitiesOCS.java @@ -30,5 +30,5 @@ import org.parceler.Parcel; @JsonObject public class CapabilitiesOCS extends GenericOCS { @JsonField(name = "data") - CapabilitiesList data; + public CapabilitiesList data; } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/CapabilitiesOverall.java b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/CapabilitiesOverall.java index c00305224..93b260477 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/CapabilitiesOverall.java +++ b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/CapabilitiesOverall.java @@ -29,5 +29,5 @@ import org.parceler.Parcel; @JsonObject public class CapabilitiesOverall { @JsonField(name = "ocs") - CapabilitiesOCS ocs; + public CapabilitiesOCS ocs; } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/SpreedCapability.java b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/SpreedCapability.java index c515508ea..d15065034 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/SpreedCapability.java +++ b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/SpreedCapability.java @@ -32,8 +32,8 @@ import org.parceler.Parcel; @JsonObject public class SpreedCapability { @JsonField(name = "features") - List features; + public List features; @JsonField(name = "config") - HashMap> config; + public HashMap> config; } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.java b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.java index dfc28b17f..729679f3f 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.java +++ b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.java @@ -29,6 +29,7 @@ import com.nextcloud.talk.R; import com.nextcloud.talk.application.NextcloudTalkApplication; import com.nextcloud.talk.models.database.UserEntity; import com.nextcloud.talk.models.json.converters.EnumSystemMessageTypeConverter; +import com.nextcloud.talk.newarch.local.models.UserNgEntity; import com.nextcloud.talk.utils.ApiUtils; import com.nextcloud.talk.utils.TextMatchers; import com.stfalcon.chatkit.commons.models.IMessage; @@ -48,13 +49,13 @@ import org.parceler.Parcel; public class ChatMessage implements IMessage, MessageContentType, MessageContentType.Image { @JsonIgnore @Ignore - public boolean isGrouped; + public boolean grouped; @JsonIgnore @Ignore - public boolean isOneToOneConversation; + public boolean oneToOneConversation; @JsonIgnore @Ignore - public UserEntity activeUser; + public UserNgEntity activeUser; @JsonIgnore @Ignore public Map selectedIndividualHashMap; diff --git a/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.kt b/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.kt index 0bf5c6c7e..595fd4a75 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/conversations/Conversation.kt @@ -34,6 +34,8 @@ import com.nextcloud.talk.models.json.converters.EnumParticipantTypeConverter import com.nextcloud.talk.models.json.converters.EnumReadOnlyConversationConverter import com.nextcloud.talk.models.json.converters.EnumRoomTypeConverter import com.nextcloud.talk.models.json.participants.Participant +import com.nextcloud.talk.newarch.local.models.UserNgEntity +import com.nextcloud.talk.newarch.local.models.hasSpreedFeatureCapability import lombok.Data import org.parceler.Parcel import org.parceler.ParcelConstructor @@ -120,36 +122,36 @@ class Conversation { return resources.getString(R.string.nc_delete_conversation_default) } - private fun isLockedOneToOne(conversationUser: UserEntity): Boolean { + private fun isLockedOneToOne(conversationUser: UserNgEntity): Boolean { return type == ConversationType.ONE_TO_ONE_CONVERSATION && conversationUser .hasSpreedFeatureCapability( "locked-one-to-one-rooms" ) } - fun canModerate(conversationUser: UserEntity): Boolean { + fun canModerate(conversationUser: UserNgEntity): Boolean { return (Participant.ParticipantType.OWNER == participantType || Participant.ParticipantType.MODERATOR == participantType) && !isLockedOneToOne( conversationUser ) } - fun shouldShowLobby(conversationUser: UserEntity): Boolean { + fun shouldShowLobby(conversationUser: UserNgEntity): Boolean { return LobbyState.LOBBY_STATE_MODERATORS_ONLY == lobbyState && !canModerate( conversationUser ) } - fun isLobbyViewApplicable(conversationUser: UserEntity): Boolean { + fun isLobbyViewApplicable(conversationUser: UserNgEntity): Boolean { return !canModerate( conversationUser ) && (type == ConversationType.GROUP_CONVERSATION || type == ConversationType.PUBLIC_CONVERSATION) } - fun isNameEditable(conversationUser: UserEntity): Boolean { + fun isNameEditable(conversationUser: UserNgEntity): Boolean { return canModerate(conversationUser) && ConversationType.ONE_TO_ONE_CONVERSATION != type } - fun canLeave(conversationUser: UserEntity): Boolean { + fun canLeave(conversationUser: UserNgEntity): Boolean { return !canModerate( conversationUser ) || type != ConversationType.ONE_TO_ONE_CONVERSATION && participants!!.size > 1 diff --git a/app/src/main/java/com/nextcloud/talk/models/json/push/PushConfigurationState.java b/app/src/main/java/com/nextcloud/talk/models/json/push/PushConfigurationState.kt similarity index 51% rename from app/src/main/java/com/nextcloud/talk/models/json/push/PushConfigurationState.java rename to app/src/main/java/com/nextcloud/talk/models/json/push/PushConfigurationState.kt index 29cc3dc59..0221c872a 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/push/PushConfigurationState.java +++ b/app/src/main/java/com/nextcloud/talk/models/json/push/PushConfigurationState.kt @@ -18,29 +18,28 @@ * along with this program. If not, see . */ -package com.nextcloud.talk.models.json.push; +package com.nextcloud.talk.models.json.push -import com.bluelinelabs.logansquare.annotation.JsonField; -import com.bluelinelabs.logansquare.annotation.JsonObject; -import lombok.Data; -import org.parceler.Parcel; +import android.os.Parcelable +import com.bluelinelabs.logansquare.annotation.JsonField +import com.bluelinelabs.logansquare.annotation.JsonObject +import kotlinx.android.parcel.Parcelize +import lombok.Data +import org.parceler.Parcel @Parcel @Data @JsonObject -public class PushConfigurationState { - @JsonField(name = "pushToken") - public String pushToken; - - @JsonField(name = "deviceIdentifier") - public String deviceIdentifier; - - @JsonField(name = "deviceIdentifierSignature") - public String deviceIdentifierSignature; - - @JsonField(name = "userPublicKey") - public String userPublicKey; - - @JsonField(name = "usesRegularPass") - public boolean usesRegularPass; -} +@Parcelize +class PushConfigurationState( + @JsonField(name = ["pushToken"]) + var pushToken: String? = null, + @JsonField(name = ["deviceIdentifier"]) + var deviceIdentifier: String? = null, + @JsonField(name = ["deviceIdentifierSignature"]) + var deviceIdentifierSignature: String? = null, + @JsonField(name = ["userPublicKey"]) + var userPublicKey: String? = null, + @JsonField(name = ["usesRegularPass"]) + var usesRegularPass: Boolean = false +): Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/models/json/push/PushRegistration.java b/app/src/main/java/com/nextcloud/talk/models/json/push/PushRegistration.java index e38580186..8ec280d58 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/push/PushRegistration.java +++ b/app/src/main/java/com/nextcloud/talk/models/json/push/PushRegistration.java @@ -30,12 +30,12 @@ import org.parceler.Parcel; @JsonObject public class PushRegistration { @JsonField(name = "publicKey") - String publicKey; + public String publicKey; @JsonField(name = "deviceIdentifier") - String deviceIdentifier; + public String deviceIdentifier; @JsonField(name = "signature") - String signature; + public String signature; } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/push/PushRegistrationOCS.java b/app/src/main/java/com/nextcloud/talk/models/json/push/PushRegistrationOCS.java index 517ba3361..d5ad0a0a3 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/push/PushRegistrationOCS.java +++ b/app/src/main/java/com/nextcloud/talk/models/json/push/PushRegistrationOCS.java @@ -31,5 +31,5 @@ import org.parceler.Parcel; @JsonObject public class PushRegistrationOCS extends GenericOCS { @JsonField(name = "data") - PushRegistration data; + public PushRegistration data; } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/push/PushRegistrationOverall.java b/app/src/main/java/com/nextcloud/talk/models/json/push/PushRegistrationOverall.java index 36e7c9103..fced72542 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/push/PushRegistrationOverall.java +++ b/app/src/main/java/com/nextcloud/talk/models/json/push/PushRegistrationOverall.java @@ -30,5 +30,5 @@ import org.parceler.Parcel; @JsonObject public class PushRegistrationOverall { @JsonField(name = "ocs") - PushRegistrationOCS ocs; + public PushRegistrationOCS ocs; } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/signaling/DataChannelMessageNick.java b/app/src/main/java/com/nextcloud/talk/models/json/signaling/DataChannelMessageNick.java index 09d40a610..35bc6c8a8 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/signaling/DataChannelMessageNick.java +++ b/app/src/main/java/com/nextcloud/talk/models/json/signaling/DataChannelMessageNick.java @@ -31,11 +31,11 @@ import org.parceler.ParcelPropertyConverter; @JsonObject public class DataChannelMessageNick { @JsonField(name = "type") - String type; + public String type; @ParcelPropertyConverter(ObjectParcelConverter.class) @JsonField(name = "payload") - HashMap payload; + public HashMap payload; public DataChannelMessageNick(String type) { this.type = type; diff --git a/app/src/main/java/com/nextcloud/talk/models/json/signaling/NCIceCandidate.java b/app/src/main/java/com/nextcloud/talk/models/json/signaling/NCIceCandidate.java index d343fd95d..c4e393b81 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/signaling/NCIceCandidate.java +++ b/app/src/main/java/com/nextcloud/talk/models/json/signaling/NCIceCandidate.java @@ -30,11 +30,11 @@ import org.parceler.Parcel; @Parcel public class NCIceCandidate { @JsonField(name = "sdpMLineIndex") - int sdpMLineIndex; + public int sdpMLineIndex; @JsonField(name = "sdpMid") - String sdpMid; + public String sdpMid; @JsonField(name = "candidate") - String candidate; + public String candidate; } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/signaling/NCMessagePayload.java b/app/src/main/java/com/nextcloud/talk/models/json/signaling/NCMessagePayload.java index f5e7b125d..c2743011e 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/signaling/NCMessagePayload.java +++ b/app/src/main/java/com/nextcloud/talk/models/json/signaling/NCMessagePayload.java @@ -30,17 +30,17 @@ import org.parceler.Parcel; @Parcel public class NCMessagePayload { @JsonField(name = "type") - String type; + public String type; @JsonField(name = "sdp") - String sdp; + public String sdp; @JsonField(name = "nick") - String nick; + public String nick; @JsonField(name = "candidate") - NCIceCandidate iceCandidate; + public NCIceCandidate iceCandidate; @JsonField(name = "name") - String name; + public String name; } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/signaling/NCMessageWrapper.java b/app/src/main/java/com/nextcloud/talk/models/json/signaling/NCMessageWrapper.java index 5648fe3c1..d27969ba1 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/signaling/NCMessageWrapper.java +++ b/app/src/main/java/com/nextcloud/talk/models/json/signaling/NCMessageWrapper.java @@ -30,12 +30,12 @@ import org.parceler.Parcel; @Parcel public class NCMessageWrapper { @JsonField(name = "fn") - NCSignalingMessage signalingMessage; + public NCSignalingMessage signalingMessage; // always a "message" @JsonField(name = "ev") - String ev; + public String ev; @JsonField(name = "sessionId") - String sessionId; + public String sessionId; } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/signaling/NCSignalingMessage.java b/app/src/main/java/com/nextcloud/talk/models/json/signaling/NCSignalingMessage.java index adf327717..6f6c78a04 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/signaling/NCSignalingMessage.java +++ b/app/src/main/java/com/nextcloud/talk/models/json/signaling/NCSignalingMessage.java @@ -30,17 +30,17 @@ import org.parceler.Parcel; @Parcel public class NCSignalingMessage { @JsonField(name = "from") - String from; + public String from; @JsonField(name = "to") - String to; + public String to; @JsonField(name = "type") - String type; + public String type; @JsonField(name = "payload") - NCMessagePayload payload; + public NCMessagePayload payload; @JsonField(name = "roomType") - String roomType; + public String roomType; @JsonField(name = "sid") - String sid; + public String sid; @JsonField(name = "prefix") - String prefix; + public String prefix; } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/signaling/Signaling.java b/app/src/main/java/com/nextcloud/talk/models/json/signaling/Signaling.java index 77586cc56..a51dd2d31 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/signaling/Signaling.java +++ b/app/src/main/java/com/nextcloud/talk/models/json/signaling/Signaling.java @@ -32,8 +32,8 @@ import lombok.Data; @JsonObject public class Signaling { @JsonField(name = "type") - String type; + public String type; //can be NCMessageWrapper or List> @JsonField(name = "data") - Object messageWrapper; + public Object messageWrapper; } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/signaling/SignalingOCS.java b/app/src/main/java/com/nextcloud/talk/models/json/signaling/SignalingOCS.java index 29d7f7115..8d6a98ede 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/signaling/SignalingOCS.java +++ b/app/src/main/java/com/nextcloud/talk/models/json/signaling/SignalingOCS.java @@ -30,5 +30,5 @@ import lombok.Data; @JsonObject public class SignalingOCS extends GenericOCS { @JsonField(name = "data") - List signalings; + public List signalings; } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/signaling/SignalingOverall.java b/app/src/main/java/com/nextcloud/talk/models/json/signaling/SignalingOverall.java index e4df03217..414cc3e4e 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/signaling/SignalingOverall.java +++ b/app/src/main/java/com/nextcloud/talk/models/json/signaling/SignalingOverall.java @@ -28,5 +28,5 @@ import lombok.Data; @Data public class SignalingOverall { @JsonField(name = "ocs") - SignalingOCS ocs; + public SignalingOCS ocs; } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/IceServer.java b/app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/IceServer.java index f8f96156f..10759e64d 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/IceServer.java +++ b/app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/IceServer.java @@ -29,14 +29,14 @@ import lombok.Data; @JsonObject public class IceServer { @JsonField(name = "url") - String url; + public String url; @JsonField(name = "urls") - List urls; + public List urls; @JsonField(name = "username") - String username; + public String username; @JsonField(name = "credential") - String credential; + public String credential; } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/Settings.java b/app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/Settings.java index 6004959cf..d961e3130 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/Settings.java +++ b/app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/Settings.java @@ -29,14 +29,14 @@ import lombok.Data; @JsonObject public class Settings { @JsonField(name = "stunservers") - List stunServers; + public List stunServers; @JsonField(name = "turnservers") - List turnServers; + public List turnServers; @JsonField(name = "server") - String externalSignalingServer; + public String externalSignalingServer; @JsonField(name = "ticket") - String externalSignalingTicket; + public String externalSignalingTicket; } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/SignalingSettingsOcs.java b/app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/SignalingSettingsOcs.java index 2aa60d1b6..ebbeebb25 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/SignalingSettingsOcs.java +++ b/app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/SignalingSettingsOcs.java @@ -29,5 +29,5 @@ import lombok.Data; @JsonObject public class SignalingSettingsOcs extends GenericOCS { @JsonField(name = "data") - Settings settings; + public Settings settings; } diff --git a/app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/SignalingSettingsOverall.java b/app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/SignalingSettingsOverall.java index 99b947603..b76d00065 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/SignalingSettingsOverall.java +++ b/app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/SignalingSettingsOverall.java @@ -28,5 +28,5 @@ import lombok.Data; @JsonObject public class SignalingSettingsOverall { @JsonField(name = "ocs") - SignalingSettingsOcs ocs; + public SignalingSettingsOcs ocs; } diff --git a/app/src/main/java/com/nextcloud/talk/newarch/data/repository/offline/UsersRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/newarch/data/repository/offline/UsersRepositoryImpl.kt index 3c45dedc5..67b534f1b 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/data/repository/offline/UsersRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/data/repository/offline/UsersRepositoryImpl.kt @@ -20,9 +20,21 @@ package com.nextcloud.talk.newarch.data.repository.offline +import androidx.lifecycle.LiveData import com.nextcloud.talk.newarch.domain.repository.offline.UsersRepository import com.nextcloud.talk.newarch.local.dao.UsersDao +import com.nextcloud.talk.newarch.local.models.UserNgEntity class UsersRepositoryImpl(val usersDao: UsersDao): UsersRepository { + override fun getActiveUserLiveData(): LiveData { + return usersDao.getActiveUserLiveData() + } + override fun getActiveUser(): UserNgEntity { + return usersDao.getActiveUser() + } + + override fun getUsers(): List { + return usersDao.getUsers() + } } diff --git a/app/src/main/java/com/nextcloud/talk/newarch/data/repository/online/NextcloudTalkRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/newarch/data/repository/online/NextcloudTalkRepositoryImpl.kt index 373d8c712..f7a5b70c0 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/data/repository/online/NextcloudTalkRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/data/repository/online/NextcloudTalkRepositoryImpl.kt @@ -25,12 +25,14 @@ import com.nextcloud.talk.models.json.conversations.Conversation import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.newarch.data.source.remote.ApiService import com.nextcloud.talk.newarch.domain.repository.online.NextcloudTalkRepository +import com.nextcloud.talk.newarch.local.models.UserNgEntity +import com.nextcloud.talk.newarch.local.models.getCredentials import com.nextcloud.talk.newarch.utils.getCredentials import com.nextcloud.talk.utils.ApiUtils class NextcloudTalkRepositoryImpl(private val apiService: ApiService) : NextcloudTalkRepository { override suspend fun deleteConversationForUser( - user: UserEntity, + user: UserNgEntity, conversation: Conversation ): GenericOverall { return apiService.deleteConversation( @@ -39,7 +41,7 @@ class NextcloudTalkRepositoryImpl(private val apiService: ApiService) : Nextclou } override suspend fun leaveConversationForUser( - user: UserEntity, + user: UserNgEntity, conversation: Conversation ): GenericOverall { return apiService.leaveConversation( @@ -51,7 +53,7 @@ class NextcloudTalkRepositoryImpl(private val apiService: ApiService) : Nextclou } override suspend fun setFavoriteValueForConversation( - user: UserEntity, + user: UserNgEntity, conversation: Conversation, favorite: Boolean ): GenericOverall { @@ -68,7 +70,7 @@ class NextcloudTalkRepositoryImpl(private val apiService: ApiService) : Nextclou } } - override suspend fun getConversationsForUser(user: UserEntity): List { + override suspend fun getConversationsForUser(user: UserNgEntity): List { return apiService.getConversations( user.getCredentials(), ApiUtils.getUrlForGetRooms(user.baseUrl) diff --git a/app/src/main/java/com/nextcloud/talk/newarch/domain/repository/offline/UsersRepository.kt b/app/src/main/java/com/nextcloud/talk/newarch/domain/repository/offline/UsersRepository.kt index 45fcb119c..c9a503a8b 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/domain/repository/offline/UsersRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/domain/repository/offline/UsersRepository.kt @@ -20,6 +20,11 @@ package com.nextcloud.talk.newarch.domain.repository.offline -interface UsersRepository { +import androidx.lifecycle.LiveData +import com.nextcloud.talk.newarch.local.models.UserNgEntity +interface UsersRepository { + fun getActiveUserLiveData(): LiveData + fun getActiveUser(): UserNgEntity + fun getUsers(): List } \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/newarch/domain/repository/online/NextcloudTalkRepository.kt b/app/src/main/java/com/nextcloud/talk/newarch/domain/repository/online/NextcloudTalkRepository.kt index 9e1f28cb0..dacc1c2dd 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/domain/repository/online/NextcloudTalkRepository.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/domain/repository/online/NextcloudTalkRepository.kt @@ -23,22 +23,23 @@ package com.nextcloud.talk.newarch.domain.repository.online import com.nextcloud.talk.models.database.UserEntity import com.nextcloud.talk.models.json.conversations.Conversation import com.nextcloud.talk.models.json.generic.GenericOverall +import com.nextcloud.talk.newarch.local.models.UserNgEntity interface NextcloudTalkRepository { - suspend fun getConversationsForUser(user: UserEntity): List + suspend fun getConversationsForUser(user: UserNgEntity): List suspend fun setFavoriteValueForConversation( - user: UserEntity, + user: UserNgEntity, conversation: Conversation, favorite: Boolean ): GenericOverall suspend fun deleteConversationForUser( - user: UserEntity, + user: UserNgEntity, conversation: Conversation ): GenericOverall suspend fun leaveConversationForUser( - userEntity: UserEntity, + userEntity: UserNgEntity, conversation: Conversation ): GenericOverall } diff --git a/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/ConversationListViewModelFactory.kt b/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/ConversationListViewModelFactory.kt index 905e93227..a434043a6 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/ConversationListViewModelFactory.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/ConversationListViewModelFactory.kt @@ -24,6 +24,7 @@ import android.app.Application import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.nextcloud.talk.newarch.domain.repository.offline.ConversationsRepository +import com.nextcloud.talk.newarch.domain.repository.offline.UsersRepository import com.nextcloud.talk.newarch.domain.usecases.DeleteConversationUseCase import com.nextcloud.talk.newarch.domain.usecases.GetConversationsUseCase import com.nextcloud.talk.newarch.domain.usecases.LeaveConversationUseCase @@ -37,14 +38,15 @@ class ConversationListViewModelFactory constructor( private val leaveConversationUseCase: LeaveConversationUseCase, private val deleteConversationUseCase: DeleteConversationUseCase, private val userUtils: UserUtils, - private val offlineRepository: ConversationsRepository + private val conversationsRepository: ConversationsRepository, + private val usersRepository: UsersRepository ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { return ConversationsListViewModel( application, conversationsUseCase, setConversationFavoriteValueUseCase, leaveConversationUseCase, deleteConversationUseCase, - userUtils, offlineRepository + userUtils, conversationsRepository, usersRepository ) as T } } diff --git a/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/ConversationsListView.kt b/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/ConversationsListView.kt index 80721b3e4..a14866f7b 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/ConversationsListView.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/ConversationsListView.kt @@ -310,11 +310,6 @@ class ConversationsListView : BaseView(), OnQueryTextListener, recyclerViewAdapter.setFilter(it) recyclerViewAdapter.filterItems(500) }) - - - currentUserAvatar.observe(this@ConversationsListView, Observer { - settingsItem?.icon = it - }) } return super.onCreateView(inflater, container) @@ -452,12 +447,12 @@ class ConversationsListView : BaseView(), OnQueryTextListener, val conversation = (clickedItem as ConversationItem).model val bundle = Bundle() - bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, viewModel.currentUserLiveData.value) + bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, Parcels.wrap(viewModel.currentUserLiveData.value)) bundle.putString(BundleKeys.KEY_ROOM_TOKEN, conversation.token) bundle.putString(BundleKeys.KEY_ROOM_ID, conversation.conversationId) bundle.putParcelable(BundleKeys.KEY_ACTIVE_CONVERSATION, Parcels.wrap(conversation)) ConductorRemapping.remapChatController( - router, viewModel.currentUserLiveData.value!!.id, conversation!!.token!!, + router, viewModel.currentUserLiveData.value!!.id!!, conversation.token!!, bundle, false ) } diff --git a/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/ConversationsListViewModel.kt b/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/ConversationsListViewModel.kt index d00fb869a..59ba36b44 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/ConversationsListViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/ConversationsListViewModel.kt @@ -22,7 +22,7 @@ package com.nextcloud.talk.newarch.features.conversationsList import android.app.Application import android.content.Intent -import android.graphics.drawable.Drawable +import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Transformations import androidx.lifecycle.viewModelScope @@ -30,17 +30,18 @@ import com.nextcloud.talk.R import com.nextcloud.talk.R.drawable import com.nextcloud.talk.R.string import com.nextcloud.talk.controllers.bottomsheet.items.BasicListItemWithImage -import com.nextcloud.talk.models.database.UserEntity import com.nextcloud.talk.models.json.conversations.Conversation import com.nextcloud.talk.models.json.generic.GenericOverall import com.nextcloud.talk.newarch.conversationsList.mvp.BaseViewModel import com.nextcloud.talk.newarch.data.model.ErrorModel import com.nextcloud.talk.newarch.domain.repository.offline.ConversationsRepository +import com.nextcloud.talk.newarch.domain.repository.offline.UsersRepository import com.nextcloud.talk.newarch.domain.usecases.DeleteConversationUseCase import com.nextcloud.talk.newarch.domain.usecases.GetConversationsUseCase import com.nextcloud.talk.newarch.domain.usecases.LeaveConversationUseCase import com.nextcloud.talk.newarch.domain.usecases.SetConversationFavoriteValueUseCase import com.nextcloud.talk.newarch.domain.usecases.base.UseCaseResponse +import com.nextcloud.talk.newarch.local.models.UserNgEntity import com.nextcloud.talk.newarch.utils.ViewState.LOADING import com.nextcloud.talk.utils.ShareUtils import com.nextcloud.talk.utils.database.user.UserUtils @@ -54,26 +55,18 @@ class ConversationsListViewModel constructor( private val leaveConversationUseCase: LeaveConversationUseCase, private val deleteConversationUseCase: DeleteConversationUseCase, private val userUtils: UserUtils, - private val offlineRepository: ConversationsRepository + private val conversationsRepository: ConversationsRepository, + usersRepository: UsersRepository ) : BaseViewModel(application) { val viewState = MutableLiveData(LOADING) var messageData: String? = null val searchQuery = MutableLiveData() - val currentUserLiveData: MutableLiveData = MutableLiveData() + val currentUserLiveData = usersRepository.getActiveUserLiveData() val conversationsLiveData = Transformations.switchMap(currentUserLiveData) { - offlineRepository.getConversationsForUser(it.id) + conversationsRepository.getConversationsForUser(it.id) } - var currentUserAvatar: MutableLiveData = MutableLiveData() - get() { - if (field.value == null) { - field.value = context.resources.getDrawable(drawable.ic_settings_white_24dp) - } - - return field - } - fun leaveConversation(conversation: Conversation) { viewModelScope.launch { setConversationUpdateStatus(conversation, true) @@ -85,7 +78,7 @@ class ConversationsListViewModel constructor( ), object : UseCaseResponse { override suspend fun onSuccess(result: GenericOverall) { - offlineRepository.deleteConversation( + conversationsRepository.deleteConversation( currentUserLiveData.value!!.id, conversation .conversationId!! ) @@ -114,7 +107,7 @@ class ConversationsListViewModel constructor( ), object : UseCaseResponse { override suspend fun onSuccess(result: GenericOverall) { - offlineRepository.deleteConversation( + conversationsRepository.deleteConversation( currentUserLiveData.value!!.id, conversation .conversationId!! ) @@ -145,7 +138,7 @@ class ConversationsListViewModel constructor( ), object : UseCaseResponse { override suspend fun onSuccess(result: GenericOverall) { - offlineRepository.setFavoriteValueForConversation( + conversationsRepository.setFavoriteValueForConversation( currentUserLiveData.value!!.id, conversation.conversationId!!, favorite ) @@ -161,22 +154,18 @@ class ConversationsListViewModel constructor( } fun loadConversations() { - val userChanged = !(currentUserLiveData.value?.equals(userUtils.currentUser) ?: false) - - if (userChanged) { - currentUserLiveData.value = userUtils.currentUser - viewState.value = LOADING - } - getConversationsUseCase.invoke(viewModelScope, parametersOf(currentUserLiveData.value), object : UseCaseResponse> { override suspend fun onSuccess(result: List) { val mutableList = result.toMutableList() + val internalUserId = currentUserLiveData.value!!.id mutableList.forEach { - it.internalUserId = currentUserLiveData.value!!.id + it.internalUserId = internalUserId } - offlineRepository.saveConversationsForUser(currentUserLiveData.value!!.id, mutableList) + conversationsRepository.saveConversationsForUser( + internalUserId, + mutableList) messageData = "" } @@ -266,7 +255,7 @@ class ConversationsListViewModel constructor( conversation: Conversation, value: Boolean ) { - offlineRepository.setChangingValueForConversation( + conversationsRepository.setChangingValueForConversation( currentUserLiveData.value!!.id, conversation .conversationId!!, value ) diff --git a/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/di/module/ConversationsListModule.kt b/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/di/module/ConversationsListModule.kt index 5019a3cff..cc8539253 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/di/module/ConversationsListModule.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/features/conversationsList/di/module/ConversationsListModule.kt @@ -23,6 +23,7 @@ package com.nextcloud.talk.newarch.features.conversationsList.di.module import android.app.Application import com.nextcloud.talk.newarch.data.source.remote.ApiErrorHandler import com.nextcloud.talk.newarch.domain.repository.offline.ConversationsRepository +import com.nextcloud.talk.newarch.domain.repository.offline.UsersRepository import com.nextcloud.talk.newarch.domain.repository.online.NextcloudTalkRepository import com.nextcloud.talk.newarch.domain.usecases.DeleteConversationUseCase import com.nextcloud.talk.newarch.domain.usecases.GetConversationsUseCase @@ -42,7 +43,7 @@ val ConversationsListModule = module { factory { createConversationListViewModelFactory( androidApplication(), get(), get(), get(), get - (), get(), get() + (), get(), get(), get() ) } } @@ -83,11 +84,12 @@ fun createConversationListViewModelFactory( leaveConversationUseCase: LeaveConversationUseCase, deleteConversationUseCase: DeleteConversationUseCase, userUtils: UserUtils, - offlineRepository: ConversationsRepository + conversationsRepository: ConversationsRepository, + usersRepository: UsersRepository ): ConversationListViewModelFactory { return ConversationListViewModelFactory( application, getConversationsUseCase, setConversationFavoriteValueUseCase, leaveConversationUseCase, deleteConversationUseCase, - userUtils, offlineRepository + userUtils, conversationsRepository, usersRepository ) } \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/newarch/local/dao/UsersDao.kt b/app/src/main/java/com/nextcloud/talk/newarch/local/dao/UsersDao.kt index f44754632..824fc5a19 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/local/dao/UsersDao.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/local/dao/UsersDao.kt @@ -20,14 +20,24 @@ package com.nextcloud.talk.newarch.local.dao +import androidx.lifecycle.LiveData import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query +import com.nextcloud.talk.models.database.UserEntity +import com.nextcloud.talk.newarch.local.models.ConversationEntity import com.nextcloud.talk.newarch.local.models.UserNgEntity @Dao abstract class UsersDao { + // get active user + @Query("SELECT * FROM users where status = 1") + abstract fun getActiveUser(): UserNgEntity + + @Query("SELECT * FROM users WHERE status = 1") + abstract fun getActiveUserLiveData(): LiveData + @Query("DELETE FROM users WHERE id = :userId") abstract fun deleteUserForId(userId: Long) @@ -41,6 +51,7 @@ abstract class UsersDao { @Query("SELECT * FROM users where status != 2") abstract fun getUsers(): List + @Query("SELECT * FROM users where status = 2") abstract fun getUsersScheduledForDeletion(): List diff --git a/app/src/main/java/com/nextcloud/talk/newarch/local/models/ConversationEntity.kt b/app/src/main/java/com/nextcloud/talk/newarch/local/models/ConversationEntity.kt index c81dbd7c9..98be48b5c 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/local/models/ConversationEntity.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/local/models/ConversationEntity.kt @@ -37,17 +37,18 @@ import java.util.HashMap @Entity( tableName = "conversations", - indices = [Index(value = ["user"])], + indices = [Index(value = ["user", "conversation_id"], unique = true)], foreignKeys = [ForeignKey( entity = UserNgEntity::class, parentColumns = arrayOf("id"), childColumns = arrayOf("user"), onDelete = CASCADE, + onUpdate = CASCADE, deferred = true )] ) data class ConversationEntity( - @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") var id: Long? = null, + @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") var id: Long? = 0, @ColumnInfo(name = "user") var user: Long?, @ColumnInfo(name = "conversation_id") var conversationId: String?, @ColumnInfo(name = "token") var token: String? = null, @@ -77,7 +78,7 @@ data class ConversationEntity( ) var conversationReadOnlyState: ConversationReadOnlyState? = null, @ColumnInfo(name = "lobby_state") var lobbyState: LobbyState? = null, @ColumnInfo(name = "lobby_timer") var lobbyTimer: Long? = null, - @ColumnInfo(name = "last_read_message_id") var lastReadMessageId: Long = 0, + @ColumnInfo(name = "last_read_message") var lastReadMessageId: Long = 0, @ColumnInfo(name = "modified_at") var modifiedAt: Long? = null, @ColumnInfo(name = "changing") var changing: Boolean = false ) @@ -116,8 +117,7 @@ fun ConversationEntity.toConversation(): Conversation { } fun Conversation.toConversationEntity(): ConversationEntity { - val conversationEntity = - ConversationEntity(this.internalId, this.internalUserId, this.conversationId) + val conversationEntity = ConversationEntity(null, this.internalUserId, this.conversationId) conversationEntity.token = this.token conversationEntity.name = this.name conversationEntity.displayName = this.displayName diff --git a/app/src/main/java/com/nextcloud/talk/newarch/local/models/MessageEntity.kt b/app/src/main/java/com/nextcloud/talk/newarch/local/models/MessageEntity.kt index 1caf23c77..2667bfcca 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/local/models/MessageEntity.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/local/models/MessageEntity.kt @@ -20,9 +20,13 @@ package com.nextcloud.talk.newarch.local.models +import android.os.Parcel +import android.os.Parcelable +import android.os.Parcelable.Creator import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.ForeignKey +import androidx.room.ForeignKey.CASCADE import androidx.room.Index import androidx.room.PrimaryKey import androidx.room.RoomWarnings @@ -31,18 +35,18 @@ import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType @Entity( tableName = "messages", - indices = [Index(value = ["conversation"]), Index(value = ["user", "conversation"])], + indices = [Index(value = ["conversation"])], foreignKeys = [ForeignKey( entity = ConversationEntity::class, parentColumns = arrayOf("id"), childColumns = arrayOf("conversation"), - onDelete = ForeignKey.CASCADE, + onDelete = CASCADE, + onUpdate = CASCADE, deferred = true )] ) data class MessageEntity( @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") var id: Long? = null, - @ColumnInfo(name = "user") var user: Long? = 0, @ColumnInfo(name = "conversation") var conversation: Long? = null, @ColumnInfo(name = "message_id") var messageId: Long = 0, @ColumnInfo(name = "actor_id") var actorId: String? = null, @@ -59,7 +63,6 @@ data class MessageEntity( fun MessageEntity.toChatMessage(): ChatMessage { val chatMessage = ChatMessage() chatMessage.internalMessageId = this.id - chatMessage.internalUserId = this.user chatMessage.internalConversationId = this.conversation chatMessage.jsonMessageId = this.messageId chatMessage.actorType = this.actorType @@ -74,9 +77,7 @@ fun MessageEntity.toChatMessage(): ChatMessage { @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH) fun ChatMessage.toMessageEntity(): MessageEntity { - val messageEntity = MessageEntity() - messageEntity.id = this.internalMessageId - messageEntity.user = this.internalUserId + val messageEntity = MessageEntity(this.internalMessageId) messageEntity.conversation = this.internalConversationId messageEntity.messageId = this.jsonMessageId messageEntity.actorType = this.actorType @@ -88,4 +89,4 @@ fun ChatMessage.toMessageEntity(): MessageEntity { //messageEntity.messageParameters = this.messageParameters return messageEntity -} \ No newline at end of file +} diff --git a/app/src/main/java/com/nextcloud/talk/newarch/local/models/UserNgEntity.kt b/app/src/main/java/com/nextcloud/talk/newarch/local/models/UserNgEntity.kt index d7cb94e87..949b1bec8 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/local/models/UserNgEntity.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/local/models/UserNgEntity.kt @@ -20,6 +20,7 @@ package com.nextcloud.talk.newarch.local.models +import android.os.Parcelable import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey @@ -27,17 +28,68 @@ import com.nextcloud.talk.models.ExternalSignalingServer import com.nextcloud.talk.models.json.capabilities.Capabilities import com.nextcloud.talk.models.json.push.PushConfigurationState import com.nextcloud.talk.newarch.local.models.other.UserStatus +import com.nextcloud.talk.utils.ApiUtils +import kotlinx.android.parcel.Parcelize +import kotlinx.android.parcel.RawValue +import kotlinx.android.parcel.WriteWith +@Parcelize @Entity(tableName = "users") data class UserNgEntity( - @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") var id: Long? = null, - @ColumnInfo(name = "user_id") var userId: String? = null, - @ColumnInfo(name = "username") var username: String? = null, + @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") var id: Long, + @ColumnInfo(name = "user_id") var userId: String, + @ColumnInfo(name = "username") var username: String, + @ColumnInfo(name = "base_url") var baseUrl: String, @ColumnInfo(name = "token") var token: String? = null, @ColumnInfo(name = "display_name") var displayName: String? = null, - @ColumnInfo(name = "push_configuration") var pushConfiguration: PushConfigurationState? = null, - @ColumnInfo(name = "capabilities") var capabilities: Capabilities? = null, + @ColumnInfo( + name = "push_configuration" + ) var pushConfiguration: PushConfigurationState? = null, + @ColumnInfo(name = "capabilities") var capabilities: @RawValue Capabilities? = null, @ColumnInfo(name = "client_auth_cert") var clientCertificate: String? = null, - @ColumnInfo(name = "external_signaling") var externalSignaling: ExternalSignalingServer? = null, + @ColumnInfo( + name = "external_signaling" + ) var externalSignaling: ExternalSignalingServer? = null, @ColumnInfo(name = "status") var status: UserStatus? = null -) \ No newline at end of file +) : Parcelable { + fun hasSpreedFeatureCapability(capabilityName: String): Boolean { + val capabilityExists = capabilities?.spreedCapability?.features?.contains(capabilityName) + if (capabilityExists != null) { + return capabilityExists + } else { + return false + } + + } +} + +fun UserNgEntity.getCredentials() = ApiUtils.getCredentials(username, token) + +fun UserNgEntity.hasExternalCapability(capabilityName: String): Boolean { + val capabilityExists = capabilities?.externalCapability?.get("v1") + ?.contains(capabilityName) + if (capabilityExists != null) { + return capabilityExists + } else { + return false + } +} + +fun UserNgEntity.hasSpreedFeatureCapability(capabilityName: String): Boolean { + val capabilityExists = capabilities?.spreedCapability?.features?.contains(capabilityName) + if (capabilityExists != null) { + return capabilityExists + } else { + return false + } +} + +fun UserNgEntity.maxMessageLength(): Int { + val maxLength = capabilities?.spreedCapability?.config?.get("chat") + ?.get("max-length") + if (maxLength != null) { + return maxLength.toInt() + } else { + return 1000 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/newarch/utils/Extensions.kt b/app/src/main/java/com/nextcloud/talk/newarch/utils/Extensions.kt index 474e583c2..382b5e7be 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/utils/Extensions.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/utils/Extensions.kt @@ -21,6 +21,7 @@ package com.nextcloud.talk.newarch.utils import com.nextcloud.talk.models.database.UserEntity +import com.nextcloud.talk.newarch.local.models.UserNgEntity import com.nextcloud.talk.utils.ApiUtils fun UserEntity.getCredentials() = ApiUtils.getCredentials(username, token) diff --git a/app/src/main/java/com/nextcloud/talk/newarch/utils/Images.kt b/app/src/main/java/com/nextcloud/talk/newarch/utils/Images.kt index bec9e2f72..1b6aabc8f 100644 --- a/app/src/main/java/com/nextcloud/talk/newarch/utils/Images.kt +++ b/app/src/main/java/com/nextcloud/talk/newarch/utils/Images.kt @@ -27,6 +27,8 @@ import coil.request.LoadRequest import coil.target.Target import coil.transform.Transformation import com.nextcloud.talk.models.database.UserEntity +import com.nextcloud.talk.newarch.local.models.UserNgEntity +import com.nextcloud.talk.newarch.local.models.getCredentials class Images { fun getRequestForUrl( @@ -34,7 +36,7 @@ class Images { context: Context, url: String, userEntity: - UserEntity?, + UserNgEntity?, target: Target?, lifecycleOwner: LifecycleOwner?, vararg transformations: Transformation diff --git a/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.kt index 17f3c0c4e..03c8f10fd 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.kt @@ -72,6 +72,7 @@ import com.nextcloud.talk.R import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.events.UserMentionClickEvent import com.nextcloud.talk.models.database.UserEntity +import com.nextcloud.talk.newarch.local.models.UserNgEntity import com.nextcloud.talk.newarch.utils.Images import com.nextcloud.talk.utils.text.Spans import org.greenrobot.eventbus.EventBus @@ -200,7 +201,7 @@ object DisplayUtils { context: Context, id: String, label: CharSequence, - conversationUser: UserEntity, + conversationUser: UserNgEntity, type: String, @XmlRes chipResource: Int, emojiEditText: EditText? @@ -278,7 +279,7 @@ object DisplayUtils { id: String, label: String, type: String, - conversationUser: UserEntity, + conversationUser: UserNgEntity, @XmlRes chipXmlRes: Int ): Spannable { diff --git a/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt index 77309b3d1..7927c1f4d 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/NotificationUtils.kt @@ -30,6 +30,7 @@ import android.os.Build import android.service.notification.StatusBarNotification import com.nextcloud.talk.R import com.nextcloud.talk.models.database.UserEntity +import com.nextcloud.talk.newarch.local.models.UserNgEntity import com.nextcloud.talk.utils.bundle.BundleKeys object NotificationUtils { @@ -91,7 +92,7 @@ object NotificationUtils { fun cancelAllNotificationsForAccount( context: Context?, - conversationUser: UserEntity + conversationUser: UserNgEntity ) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && conversationUser.id != -1L && context != null) { @@ -115,7 +116,7 @@ object NotificationUtils { fun cancelExistingNotificationWithId( context: Context?, - conversationUser: UserEntity, + conversationUser: UserNgEntity, notificationId: Long ) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && conversationUser.id != -1L && @@ -144,7 +145,7 @@ object NotificationUtils { fun findNotificationForRoom( context: Context?, - conversationUser: UserEntity, + conversationUser: UserNgEntity, roomTokenOrId: String ): StatusBarNotification? { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && conversationUser.id != -1L && @@ -177,7 +178,7 @@ object NotificationUtils { fun cancelExistingNotificationsForRoom( context: Context?, - conversationUser: UserEntity, + conversationUser: UserNgEntity, roomTokenOrId: String ) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && conversationUser.id != -1L && diff --git a/app/src/main/java/com/nextcloud/talk/utils/PushUtils.java b/app/src/main/java/com/nextcloud/talk/utils/PushUtils.java deleted file mode 100644 index c83d9d687..000000000 --- a/app/src/main/java/com/nextcloud/talk/utils/PushUtils.java +++ /dev/null @@ -1,445 +0,0 @@ -/* - * Nextcloud Talk application - * - * @author Mario Danic - * Copyright (C) 2017 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.utils; - -import android.content.Context; -import android.text.TextUtils; -import android.util.Base64; -import android.util.Log; -import autodagger.AutoInjector; -import com.bluelinelabs.logansquare.LoganSquare; -import com.nextcloud.talk.R; -import com.nextcloud.talk.api.NcApi; -import com.nextcloud.talk.application.NextcloudTalkApplication; -import com.nextcloud.talk.events.EventStatus; -import com.nextcloud.talk.models.SignatureVerification; -import com.nextcloud.talk.models.database.UserEntity; -import com.nextcloud.talk.models.json.push.PushConfigurationState; -import com.nextcloud.talk.models.json.push.PushRegistrationOverall; -import com.nextcloud.talk.utils.database.user.UserUtils; -import com.nextcloud.talk.utils.preferences.AppPreferences; -import io.reactivex.Observer; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.security.InvalidKeyException; -import java.security.Key; -import java.security.KeyFactory; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.PublicKey; -import java.security.Signature; -import java.security.SignatureException; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.PKCS8EncodedKeySpec; -import java.security.spec.X509EncodedKeySpec; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import javax.inject.Inject; -import org.greenrobot.eventbus.EventBus; - -@AutoInjector(NextcloudTalkApplication.class) -public class PushUtils { - private static final String TAG = "PushUtils"; - - @Inject - UserUtils userUtils; - - @Inject - AppPreferences appPreferences; - - @Inject - EventBus eventBus; - - @Inject - NcApi ncApi; - - private File keysFile; - private File publicKeyFile; - private File privateKeyFile; - - private String proxyServer; - - public PushUtils() { - NextcloudTalkApplication.Companion.getSharedApplication() - .getComponentApplication() - .inject(this); - - keysFile = NextcloudTalkApplication.Companion.getSharedApplication() - .getDir("PushKeyStore", Context.MODE_PRIVATE); - - publicKeyFile = - new File(NextcloudTalkApplication.Companion.getSharedApplication().getDir("PushKeystore", - Context.MODE_PRIVATE), "push_key.pub"); - privateKeyFile = - new File(NextcloudTalkApplication.Companion.getSharedApplication().getDir("PushKeystore", - Context.MODE_PRIVATE), "push_key.priv"); - proxyServer = NextcloudTalkApplication.Companion.getSharedApplication().getResources(). - getString(R.string.nc_push_server_url); - } - - public SignatureVerification verifySignature(byte[] signatureBytes, byte[] subjectBytes) { - Signature signature = null; - PushConfigurationState pushConfigurationState; - PublicKey publicKey; - SignatureVerification signatureVerification = new SignatureVerification(); - signatureVerification.setSignatureValid(false); - - List userEntities = userUtils.getUsers(); - try { - signature = Signature.getInstance("SHA512withRSA"); - if (userEntities != null && userEntities.size() > 0) { - for (UserEntity userEntity : userEntities) { - if (!TextUtils.isEmpty(userEntity.getPushConfigurationState())) { - pushConfigurationState = LoganSquare.parse(userEntity.getPushConfigurationState(), - PushConfigurationState.class); - publicKey = (PublicKey) readKeyFromString(true, - pushConfigurationState.getUserPublicKey()); - signature.initVerify(publicKey); - signature.update(subjectBytes); - if (signature.verify(signatureBytes)) { - signatureVerification.setSignatureValid(true); - signatureVerification.setUserEntity(userEntity); - return signatureVerification; - } - } - } - } - } catch (NoSuchAlgorithmException e) { - Log.d(TAG, "No such algorithm"); - } catch (IOException e) { - Log.d(TAG, "Error while trying to parse push configuration viewState"); - } catch (InvalidKeyException e) { - Log.d(TAG, "Invalid key while trying to verify"); - } catch (SignatureException e) { - Log.d(TAG, "Signature exception while trying to verify"); - } - - return signatureVerification; - } - - private int saveKeyToFile(Key key, String path) { - byte[] encoded = key.getEncoded(); - - try { - if (!new File(path).exists()) { - if (!new File(path).createNewFile()) { - return -1; - } - } - - try (FileOutputStream keyFileOutputStream = new FileOutputStream(path)) { - keyFileOutputStream.write(encoded); - return 0; - } - } catch (FileNotFoundException e) { - Log.d(TAG, "Failed to save key to file"); - } catch (IOException e) { - Log.d(TAG, "Failed to save key to file via IOException"); - } - - return -1; - } - - private String generateSHA512Hash(String pushToken) { - MessageDigest messageDigest = null; - try { - messageDigest = MessageDigest.getInstance("SHA-512"); - messageDigest.update(pushToken.getBytes()); - return bytesToHex(messageDigest.digest()); - } catch (NoSuchAlgorithmException e) { - Log.d(TAG, "SHA-512 algorithm not supported"); - } - return ""; - } - - private String bytesToHex(byte[] bytes) { - StringBuilder result = new StringBuilder(); - for (byte individualByte : bytes) { - result.append(Integer.toString((individualByte & 0xff) + 0x100, 16) - .substring(1)); - } - return result.toString(); - } - - public int generateRsa2048KeyPair() { - if (!publicKeyFile.exists() && !privateKeyFile.exists()) { - if (!keysFile.exists()) { - keysFile.mkdirs(); - } - - KeyPairGenerator keyGen = null; - try { - keyGen = KeyPairGenerator.getInstance("RSA"); - keyGen.initialize(2048); - - KeyPair pair = keyGen.generateKeyPair(); - int statusPrivate = saveKeyToFile(pair.getPrivate(), privateKeyFile.getAbsolutePath()); - int statusPublic = saveKeyToFile(pair.getPublic(), publicKeyFile.getAbsolutePath()); - - if (statusPrivate == 0 && statusPublic == 0) { - // all went well - return 0; - } else { - return -2; - } - } catch (NoSuchAlgorithmException e) { - Log.d(TAG, "RSA algorithm not supported"); - } - } else { - // We already have the key - return -1; - } - - // we failed to generate the key - return -2; - } - - public void pushRegistrationToServer() { - String token = appPreferences.getPushToken(); - - if (!TextUtils.isEmpty(token)) { - String credentials; - String pushTokenHash = generateSHA512Hash(token).toLowerCase(); - PublicKey devicePublicKey = (PublicKey) readKeyFromFile(true); - if (devicePublicKey != null) { - byte[] publicKeyBytes = Base64.encode(devicePublicKey.getEncoded(), Base64.NO_WRAP); - String publicKey = new String(publicKeyBytes); - publicKey = publicKey.replaceAll("(.{64})", "$1\n"); - - publicKey = "-----BEGIN PUBLIC KEY-----\n" + publicKey + "\n-----END PUBLIC KEY-----\n"; - - if (userUtils.anyUserExists()) { - String providerValue; - PushConfigurationState accountPushData = null; - for (Object userEntityObject : userUtils.getUsers()) { - UserEntity userEntity = (UserEntity) userEntityObject; - providerValue = userEntity.getPushConfigurationState(); - if (!TextUtils.isEmpty(providerValue)) { - try { - accountPushData = LoganSquare.parse(providerValue, PushConfigurationState.class); - } catch (IOException e) { - Log.d(TAG, "Failed to parse account push data"); - accountPushData = null; - } - } else { - accountPushData = null; - } - - if (((TextUtils.isEmpty(providerValue) || accountPushData == null) - && !userEntity.getScheduledForDeletion()) || - (accountPushData != null - && !accountPushData.getPushToken().equals(token) - && !userEntity.getScheduledForDeletion())) { - - Map queryMap = new HashMap<>(); - queryMap.put("format", "json"); - queryMap.put("pushTokenHash", pushTokenHash); - queryMap.put("devicePublicKey", publicKey); - queryMap.put("proxyServer", proxyServer); - - credentials = - ApiUtils.getCredentials(userEntity.getUsername(), userEntity.getToken()); - - String finalCredentials = credentials; - ncApi.registerDeviceForNotificationsWithNextcloud( - credentials, - ApiUtils.getUrlNextcloudPush(userEntity.getBaseUrl()), queryMap) - .subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - - } - - @Override - public void onNext(PushRegistrationOverall pushRegistrationOverall) { - Map proxyMap = new HashMap<>(); - proxyMap.put("pushToken", token); - proxyMap.put("deviceIdentifier", pushRegistrationOverall.getOcs().getData(). - getDeviceIdentifier()); - proxyMap.put("deviceIdentifierSignature", pushRegistrationOverall.getOcs() - .getData().getSignature()); - proxyMap.put("userPublicKey", pushRegistrationOverall.getOcs() - .getData().getPublicKey()); - - ncApi.registerDeviceForNotificationsWithProxy( - ApiUtils.getUrlPushProxy(), proxyMap) - .subscribeOn(Schedulers.io()) - .subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - - } - - @Override - public void onNext(Void aVoid) { - PushConfigurationState pushConfigurationState = - new PushConfigurationState(); - pushConfigurationState.setPushToken(token); - pushConfigurationState.setDeviceIdentifier( - pushRegistrationOverall.getOcs() - .getData().getDeviceIdentifier()); - pushConfigurationState.setDeviceIdentifierSignature( - pushRegistrationOverall - .getOcs().getData().getSignature()); - pushConfigurationState.setUserPublicKey( - pushRegistrationOverall.getOcs() - .getData().getPublicKey()); - pushConfigurationState.setUsesRegularPass(false); - - try { - userUtils.createOrUpdateUser(null, - null, null, - userEntity.getDisplayName(), - LoganSquare.serialize(pushConfigurationState), null, - null, userEntity.getId(), null, null, null) - .subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - - } - - @Override - public void onNext(UserEntity userEntity) { - eventBus.post(new EventStatus(userEntity.getId(), - EventStatus.EventType.PUSH_REGISTRATION, true)); - } - - @Override - public void onError(Throwable e) { - eventBus.post(new EventStatus - (userEntity.getId(), - EventStatus.EventType - .PUSH_REGISTRATION, false)); - } - - @Override - public void onComplete() { - - } - }); - } catch (IOException e) { - Log.e(TAG, "IOException while updating user"); - } - } - - @Override - public void onError(Throwable e) { - eventBus.post(new EventStatus(userEntity.getId(), - EventStatus.EventType.PUSH_REGISTRATION, false)); - } - - @Override - public void onComplete() { - - } - }); - } - - @Override - public void onError(Throwable e) { - eventBus.post(new EventStatus(userEntity.getId(), - EventStatus.EventType.PUSH_REGISTRATION, false)); - } - - @Override - public void onComplete() { - } - }); - } - } - } - } - } - } - - private Key readKeyFromString(boolean readPublicKey, String keyString) { - if (readPublicKey) { - keyString = keyString.replaceAll("\\n", "").replace("-----BEGIN PUBLIC KEY-----", - "").replace("-----END PUBLIC KEY-----", ""); - } else { - keyString = keyString.replaceAll("\\n", "").replace("-----BEGIN PRIVATE KEY-----", - "").replace("-----END PRIVATE KEY-----", ""); - } - - KeyFactory keyFactory = null; - try { - keyFactory = KeyFactory.getInstance("RSA"); - if (readPublicKey) { - X509EncodedKeySpec keySpec = - new X509EncodedKeySpec(Base64.decode(keyString, Base64.DEFAULT)); - return keyFactory.generatePublic(keySpec); - } else { - PKCS8EncodedKeySpec keySpec = - new PKCS8EncodedKeySpec(Base64.decode(keyString, Base64.DEFAULT)); - return keyFactory.generatePrivate(keySpec); - } - } catch (NoSuchAlgorithmException e) { - Log.d("TAG", "No such algorithm while reading key from string"); - } catch (InvalidKeySpecException e) { - Log.d("TAG", "Invalid key spec while reading key from string"); - } - - return null; - } - - public Key readKeyFromFile(boolean readPublicKey) { - String path; - - if (readPublicKey) { - path = publicKeyFile.getAbsolutePath(); - } else { - path = privateKeyFile.getAbsolutePath(); - } - - try (FileInputStream fileInputStream = new FileInputStream(path)) { - byte[] bytes = new byte[fileInputStream.available()]; - fileInputStream.read(bytes); - - KeyFactory keyFactory = KeyFactory.getInstance("RSA"); - - if (readPublicKey) { - X509EncodedKeySpec keySpec = new X509EncodedKeySpec(bytes); - return keyFactory.generatePublic(keySpec); - } else { - PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(bytes); - return keyFactory.generatePrivate(keySpec); - } - } catch (FileNotFoundException e) { - Log.d(TAG, "Failed to find path while reading the Key"); - } catch (IOException e) { - Log.d(TAG, "IOException while reading the key"); - } catch (InvalidKeySpecException e) { - Log.d(TAG, "InvalidKeySpecException while reading the key"); - } catch (NoSuchAlgorithmException e) { - Log.d(TAG, "RSA algorithm not supported"); - } - - return null; - } -} diff --git a/app/src/main/java/com/nextcloud/talk/utils/PushUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/PushUtils.kt new file mode 100644 index 000000000..8e4e82129 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/PushUtils.kt @@ -0,0 +1,450 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * Copyright (C) 2017 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.utils + +import android.content.Context +import android.text.TextUtils +import android.util.Base64 +import android.util.Log +import autodagger.AutoInjector +import com.bluelinelabs.logansquare.LoganSquare +import com.nextcloud.talk.R.string +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.events.EventStatus +import com.nextcloud.talk.events.EventStatus.EventType.PUSH_REGISTRATION +import com.nextcloud.talk.models.SignatureVerification +import com.nextcloud.talk.models.database.UserEntity +import com.nextcloud.talk.models.json.push.PushConfigurationState +import com.nextcloud.talk.models.json.push.PushRegistrationOverall +import com.nextcloud.talk.newarch.domain.repository.offline.UsersRepository +import com.nextcloud.talk.newarch.local.models.UserNgEntity +import com.nextcloud.talk.utils.database.user.UserUtils +import com.nextcloud.talk.utils.preferences.AppPreferences +import io.reactivex.Observer +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import org.greenrobot.eventbus.EventBus +import java.io.File +import java.io.FileInputStream +import java.io.FileNotFoundException +import java.io.FileOutputStream +import java.io.IOException +import java.security.InvalidKeyException +import java.security.Key +import java.security.KeyFactory +import java.security.KeyPair +import java.security.KeyPairGenerator +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException +import java.security.PublicKey +import java.security.Signature +import java.security.SignatureException +import java.security.spec.InvalidKeySpecException +import java.security.spec.PKCS8EncodedKeySpec +import java.security.spec.X509EncodedKeySpec +import java.util.HashMap +import javax.inject.Inject +import kotlin.experimental.and + +@AutoInjector(NextcloudTalkApplication::class) +class PushUtils(val usersRepository: UsersRepository) { + @JvmField + @Inject + var userUtils: UserUtils? = null + @JvmField + @Inject + var appPreferences: AppPreferences? = null + @JvmField + @Inject + var eventBus: EventBus? = null + @JvmField + @Inject + var ncApi: NcApi? = null + private val keysFile: File + private val publicKeyFile: File + private val privateKeyFile: File + private val proxyServer: String + fun verifySignature( + signatureBytes: ByteArray?, + subjectBytes: ByteArray? + ): SignatureVerification { + val signature: Signature? + var pushConfigurationState: PushConfigurationState? + var publicKey: PublicKey? + val signatureVerification = + SignatureVerification() + signatureVerification.signatureValid = false + val userEntities: List = usersRepository.getUsers() + try { + signature = Signature.getInstance("SHA512withRSA") + if (userEntities.size > 0) { + for (userEntity in userEntities) { + pushConfigurationState = userEntity.pushConfiguration + if (pushConfigurationState?.userPublicKey != null) { + publicKey = readKeyFromString( + true, pushConfigurationState.userPublicKey!! + ) as PublicKey? + signature.initVerify(publicKey) + signature.update(subjectBytes) + if (signature.verify(signatureBytes)) { + signatureVerification.signatureValid = true + signatureVerification.userEntity = userEntity + return signatureVerification + } + } + } + } + } catch (e: NoSuchAlgorithmException) { + Log.d(TAG, "No such algorithm") + } catch (e: IOException) { + Log.d(TAG, "Error while trying to parse push configuration viewState") + } catch (e: InvalidKeyException) { + Log.d(TAG, "Invalid key while trying to verify") + } catch (e: SignatureException) { + Log.d(TAG, "Signature exception while trying to verify") + } + return signatureVerification + } + + private fun saveKeyToFile( + key: Key, + path: String? + ): Int { + val encoded: ByteArray? = key.encoded + try { + if (!File(path).exists()) { + if (!File(path).createNewFile()) { + return -1 + } + } + FileOutputStream(path) + .use { keyFileOutputStream -> + keyFileOutputStream.write(encoded) + return 0 + } + } catch (e: FileNotFoundException) { + Log.d(TAG, "Failed to save key to file") + } catch (e: IOException) { + Log.d(TAG, "Failed to save key to file via IOException") + } + return -1 + } + + private fun generateSHA512Hash(pushToken: String): String { + var messageDigest: MessageDigest? = null + try { + messageDigest = MessageDigest.getInstance("SHA-512") + messageDigest.update(pushToken.toByteArray()) + return bytesToHex(messageDigest.digest()) + } catch (e: NoSuchAlgorithmException) { + Log.d(TAG, "SHA-512 algorithm not supported") + } + return "" + } + + private fun bytesToHex(bytes: ByteArray): String { + val result = StringBuilder() + for (individualByte in bytes) { + result.append( + ((individualByte and 0xff.toByte()) + 0x100).toString(16) + .substring(1) + ) + } + return result.toString() + } + + fun generateRsa2048KeyPair(): Int { + if (!publicKeyFile.exists() && !privateKeyFile.exists()) { + if (!keysFile.exists()) { + keysFile.mkdirs() + } + var keyGen: KeyPairGenerator? = null + try { + keyGen = KeyPairGenerator.getInstance("RSA") + keyGen.initialize(2048) + val pair: KeyPair = keyGen.generateKeyPair() + val statusPrivate = + saveKeyToFile(pair.private, privateKeyFile.absolutePath) + val statusPublic = + saveKeyToFile(pair.public, publicKeyFile.absolutePath) + return if (statusPrivate == 0 && statusPublic == 0) { + // all went well + + 0 + } else { + -2 + } + } catch (e: NoSuchAlgorithmException) { + Log.d(TAG, "RSA algorithm not supported") + } + } + + // we failed to generate the key + else { + // We already have the key + + return -1 + } + + return -2 + } + + fun pushRegistrationToServer() { + val token: String = appPreferences!!.pushToken + if (!TextUtils.isEmpty(token)) { + var credentials: String + val pushTokenHash = generateSHA512Hash(token).toLowerCase() + val devicePublicKey = + readKeyFromFile(true) as PublicKey? + if (devicePublicKey != null) { + val publicKeyBytes: ByteArray? = + Base64.encode(devicePublicKey.encoded, Base64.NO_WRAP) + var publicKey = String(publicKeyBytes!!) + publicKey = publicKey.replace("(.{64})".toRegex(), "$1\n") + publicKey = "-----BEGIN PUBLIC KEY-----\n$publicKey\n-----END PUBLIC KEY-----\n" + if (userUtils!!.anyUserExists()) { + var accountPushData: PushConfigurationState? = null + for (userEntityObject in usersRepository.getUsers()) { + val userEntity = userEntityObject + accountPushData = userEntity.pushConfiguration + if (accountPushData == null || accountPushData.pushToken != token) { + val queryMap: MutableMap = + HashMap() + queryMap["format"] = "json" + queryMap["pushTokenHash"] = pushTokenHash + queryMap["devicePublicKey"] = publicKey + queryMap["proxyServer"] = proxyServer + credentials = ApiUtils.getCredentials( + userEntity.username, userEntity.token + ) + val finalCredentials = credentials + ncApi!!.registerDeviceForNotificationsWithNextcloud( + credentials, + ApiUtils.getUrlNextcloudPush(userEntity.baseUrl), + queryMap + ) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) {} + override fun onNext(pushRegistrationOverall: PushRegistrationOverall) { + val proxyMap: MutableMap = + HashMap() + proxyMap["pushToken"] = token + proxyMap["deviceIdentifier"] = + pushRegistrationOverall.ocs.data.deviceIdentifier + proxyMap["deviceIdentifierSignature"] = pushRegistrationOverall.ocs + .data.signature + proxyMap["userPublicKey"] = pushRegistrationOverall.ocs + .data.publicKey + ncApi!!.registerDeviceForNotificationsWithProxy( + ApiUtils.getUrlPushProxy(), proxyMap + ) + .subscribeOn(Schedulers.io()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) {} + override fun onNext(aVoid: Void) { + val pushConfigurationState = + PushConfigurationState() + pushConfigurationState.pushToken = token + pushConfigurationState.deviceIdentifier = pushRegistrationOverall + .ocs.data.deviceIdentifier + pushConfigurationState.deviceIdentifierSignature = + pushRegistrationOverall.ocs.data.signature + pushConfigurationState.userPublicKey = pushRegistrationOverall.ocs + .data.publicKey + pushConfigurationState.usesRegularPass = false + try { + userUtils!!.createOrUpdateUser( + null, + null, null, + userEntity.displayName, + LoganSquare.serialize( + pushConfigurationState + ), null, + null, userEntity.id, null, null, null + ) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) {} + override fun onNext(userEntity: UserEntity) { + eventBus!!.post( + EventStatus( + userEntity.id, + PUSH_REGISTRATION, + true + ) + ) + } + + override fun onError(e: Throwable) { + eventBus!!.post( + EventStatus( + userEntity.id, + PUSH_REGISTRATION, false + ) + ) + } + + override fun onComplete() {} + }) + } catch (e: IOException) { + Log.e(TAG, "IOException while updating user") + } + } + + override fun onError(e: Throwable) { + eventBus!!.post( + EventStatus( + userEntity.id, + PUSH_REGISTRATION, + false + ) + ) + } + + override fun onComplete() {} + }) + } + + override fun onError(e: Throwable) { + eventBus!!.post( + EventStatus( + userEntity.id, + PUSH_REGISTRATION, + false + ) + ) + } + + override fun onComplete() {} + }) + } + } + } + } + } + } + + private fun readKeyFromString( + readPublicKey: Boolean, + keyString: String + ): Key? { + var keyString = keyString + keyString = if (readPublicKey) { + keyString.replace("\\n".toRegex(), "") + .replace( + "-----BEGIN PUBLIC KEY-----", + "" + ) + .replace("-----END PUBLIC KEY-----", "") + } else { + keyString.replace("\\n".toRegex(), "") + .replace( + "-----BEGIN PRIVATE KEY-----", + "" + ) + .replace("-----END PRIVATE KEY-----", "") + } + var keyFactory: KeyFactory? = null + try { + keyFactory = KeyFactory.getInstance("RSA") + return if (readPublicKey) { + val keySpec = X509EncodedKeySpec( + Base64.decode(keyString, Base64.DEFAULT) + ) + keyFactory.generatePublic(keySpec) + } else { + val keySpec = + PKCS8EncodedKeySpec( + Base64.decode(keyString, Base64.DEFAULT) + ) + keyFactory.generatePrivate(keySpec) + } + } catch (e: NoSuchAlgorithmException) { + Log.d("TAG", "No such algorithm while reading key from string") + } catch (e: InvalidKeySpecException) { + Log.d("TAG", "Invalid key spec while reading key from string") + } + return null + } + + fun readKeyFromFile(readPublicKey: Boolean): Key? { + val path: String? + path = if (readPublicKey) { + publicKeyFile.absolutePath + } else { + privateKeyFile.absolutePath + } + try { + FileInputStream(path) + .use { fileInputStream -> + val bytes = ByteArray(fileInputStream.available()) + fileInputStream.read(bytes) + val keyFactory: KeyFactory = KeyFactory.getInstance("RSA") + return if (readPublicKey) { + val keySpec = + X509EncodedKeySpec(bytes) + keyFactory.generatePublic(keySpec) + } else { + val keySpec = + PKCS8EncodedKeySpec(bytes) + keyFactory.generatePrivate(keySpec) + } + } + } catch (e: FileNotFoundException) { + Log.d(TAG, "Failed to find path while reading the Key") + } catch (e: IOException) { + Log.d(TAG, "IOException while reading the key") + } catch (e: InvalidKeySpecException) { + Log.d(TAG, "InvalidKeySpecException while reading the key") + } catch (e: NoSuchAlgorithmException) { + Log.d(TAG, "RSA algorithm not supported") + } + return null + } + + companion object { + private const val TAG = "PushUtils" + } + + init { + sharedApplication!! + .componentApplication + .inject(this) + keysFile = sharedApplication!! + .getDir("PushKeyStore", Context.MODE_PRIVATE) + publicKeyFile = File( + sharedApplication!!.getDir( + "PushKeystore", + Context.MODE_PRIVATE + ), "push_key.pub" + ) + privateKeyFile = File( + sharedApplication!!.getDir( + "PushKeystore", + Context.MODE_PRIVATE + ), "push_key.priv" + ) + proxyServer = + sharedApplication!!.resources + .getString(string.nc_push_server_url) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/utils/preferences/preferencestorage/DatabaseStorageFactory.java b/app/src/main/java/com/nextcloud/talk/utils/preferences/preferencestorage/DatabaseStorageFactory.java index 5f56e863d..5a5605d8c 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/preferences/preferencestorage/DatabaseStorageFactory.java +++ b/app/src/main/java/com/nextcloud/talk/utils/preferences/preferencestorage/DatabaseStorageFactory.java @@ -23,14 +23,15 @@ package com.nextcloud.talk.utils.preferences.preferencestorage; import android.content.Context; import com.nextcloud.talk.interfaces.ConversationInfoInterface; import com.nextcloud.talk.models.database.UserEntity; +import com.nextcloud.talk.newarch.local.models.UserNgEntity; import com.yarolegovich.mp.io.StorageModule; public class DatabaseStorageFactory implements StorageModule.Factory { - private UserEntity conversationUser; + private UserNgEntity conversationUser; private String conversationToken; private ConversationInfoInterface conversationInfoInterface; - public DatabaseStorageFactory(UserEntity conversationUser, String conversationToken, + public DatabaseStorageFactory(UserNgEntity conversationUser, String conversationToken, ConversationInfoInterface conversationInfoInterface) { this.conversationUser = conversationUser; this.conversationToken = conversationToken; diff --git a/app/src/main/java/com/nextcloud/talk/utils/preferences/preferencestorage/DatabaseStorageModule.java b/app/src/main/java/com/nextcloud/talk/utils/preferences/preferencestorage/DatabaseStorageModule.java deleted file mode 100644 index 67f45ee13..000000000 --- a/app/src/main/java/com/nextcloud/talk/utils/preferences/preferencestorage/DatabaseStorageModule.java +++ /dev/null @@ -1,297 +0,0 @@ -/* - * Nextcloud Talk application - * - * @author Mario Danic - * Copyright (C) 2017-2018 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.utils.preferences.preferencestorage; - -import android.os.Bundle; -import android.text.TextUtils; -import autodagger.AutoInjector; -import com.nextcloud.talk.api.NcApi; -import com.nextcloud.talk.application.NextcloudTalkApplication; -import com.nextcloud.talk.interfaces.ConversationInfoInterface; -import com.nextcloud.talk.models.database.ArbitraryStorageEntity; -import com.nextcloud.talk.models.database.UserEntity; -import com.nextcloud.talk.models.json.generic.GenericOverall; -import com.nextcloud.talk.utils.ApiUtils; -import com.nextcloud.talk.utils.database.arbitrarystorage.ArbitraryStorageUtils; -import com.yarolegovich.mp.io.StorageModule; -import io.reactivex.Observer; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; -import java.util.Set; -import javax.inject.Inject; - -@AutoInjector(NextcloudTalkApplication.class) -public class DatabaseStorageModule implements StorageModule { - @Inject - ArbitraryStorageUtils arbitraryStorageUtils; - - @Inject - NcApi ncApi; - - private UserEntity conversationUser; - private String conversationToken; - private long accountIdentifier; - - private boolean lobbyValue; - private boolean favoriteConversationValue; - private boolean allowGuestsValue; - - private Boolean hasPassword; - private String conversationNameValue; - - private String messageNotificationLevel; - private ConversationInfoInterface conversationInfoInterface; - - public DatabaseStorageModule(UserEntity conversationUser, String conversationToken, - ConversationInfoInterface conversationInfoInterface) { - NextcloudTalkApplication.Companion.getSharedApplication() - .getComponentApplication() - .inject(this); - - this.conversationUser = conversationUser; - this.accountIdentifier = conversationUser.getId(); - this.conversationToken = conversationToken; - this.conversationInfoInterface = conversationInfoInterface; - } - - @Override - public void saveBoolean(String key, boolean value) { - if (!key.equals("conversation_lobby") && !key.equals("allow_guests") && !key.equals( - "favorite_conversation")) { - arbitraryStorageUtils.storeStorageSetting(accountIdentifier, key, Boolean.toString(value), - conversationToken); - } else { - switch (key) { - case "conversation_lobby": - lobbyValue = value; - break; - case "allow_guests": - allowGuestsValue = value; - break; - case "favorite_conversation": - favoriteConversationValue = value; - break; - default: - } - } - } - - @Override - public void saveString(String key, String value) { - if (!key.equals("message_notification_level") - && !key.equals("conversation_name") - && !key.equals("conversation_password")) { - arbitraryStorageUtils.storeStorageSetting(accountIdentifier, key, value, conversationToken); - } else { - if (key.equals("message_notification_level")) { - if (conversationUser.hasSpreedFeatureCapability("notification-levels")) { - if (!TextUtils.isEmpty(messageNotificationLevel) && !messageNotificationLevel.equals( - value)) { - int intValue; - switch (value) { - case "never": - intValue = 3; - break; - case "mention": - intValue = 2; - break; - case "always": - intValue = 1; - break; - default: - intValue = 0; - } - - ncApi.setNotificationLevel( - ApiUtils.getCredentials(conversationUser.getUsername(), - conversationUser.getToken()), - ApiUtils.getUrlForSettingNotificationlevel(conversationUser.getBaseUrl(), - conversationToken), - intValue) - .subscribeOn(Schedulers.io()) - .subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - - } - - @Override - public void onNext(GenericOverall genericOverall) { - messageNotificationLevel = value; - } - - @Override - public void onError(Throwable e) { - - } - - @Override - public void onComplete() { - } - }); - } else { - messageNotificationLevel = value; - } - } - } else if (key.equals("conversation_password")) { - if (hasPassword != null) { - ncApi.setPassword(ApiUtils.getCredentials(conversationUser.getUsername(), - conversationUser.getToken()), - ApiUtils.getUrlForPassword(conversationUser.getBaseUrl(), - conversationToken), value) - .subscribeOn(Schedulers.io()) - .subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - - } - - @Override - public void onNext(GenericOverall genericOverall) { - hasPassword = !TextUtils.isEmpty(value); - conversationInfoInterface.passwordSet(TextUtils.isEmpty(value)); - } - - @Override - public void onError(Throwable e) { - - } - - @Override - public void onComplete() { - } - }); - } else { - hasPassword = Boolean.parseBoolean(value); - } - } else if (key.equals("conversation_name")) { - if (!TextUtils.isEmpty(conversationNameValue) && !conversationNameValue.equals(value)) { - ncApi.renameRoom(ApiUtils.getCredentials(conversationUser.getUsername(), - conversationUser.getToken()), ApiUtils.getRoom(conversationUser.getBaseUrl(), - conversationToken), value) - .subscribeOn(Schedulers.io()) - .subscribe(new Observer() { - @Override - public void onSubscribe(Disposable d) { - - } - - @Override - public void onNext(GenericOverall genericOverall) { - conversationNameValue = value; - conversationInfoInterface.conversationNameSet(value); - } - - @Override - public void onError(Throwable e) { - - } - - @Override - public void onComplete() { - } - }); - } else { - conversationNameValue = value; - } - } - } - } - - @Override - public void saveInt(String key, int value) { - arbitraryStorageUtils.storeStorageSetting(accountIdentifier, key, Integer.toString(value), - conversationToken); - } - - @Override - public void saveStringSet(String key, Set value) { - - } - - @Override - public boolean getBoolean(String key, boolean defaultVal) { - if (key.equals("conversation_lobby")) { - return lobbyValue; - } else if (key.equals("allow_guests")) { - return allowGuestsValue; - } else if (key.equals("favorite_conversation")) { - return favoriteConversationValue; - } else { - ArbitraryStorageEntity valueFromDb = - arbitraryStorageUtils.getStorageSetting(accountIdentifier, key, conversationToken); - if (valueFromDb == null) { - return defaultVal; - } else { - return Boolean.parseBoolean(valueFromDb.getValue()); - } - } - } - - @Override - public String getString(String key, String defaultVal) { - if (!key.equals("message_notification_level") - && !key.equals("conversation_name") - && !key.equals("conversation_password")) { - ArbitraryStorageEntity valueFromDb = - arbitraryStorageUtils.getStorageSetting(accountIdentifier, key, conversationToken); - if (valueFromDb == null) { - return defaultVal; - } else { - return valueFromDb.getValue(); - } - } else if (key.equals("message_notification_level")) { - return messageNotificationLevel; - } else if (key.equals("conversation_name")) { - return conversationNameValue; - } else if (key.equals("conversation_password")) { - return ""; - } - - return ""; - } - - @Override - public int getInt(String key, int defaultVal) { - ArbitraryStorageEntity valueFromDb = - arbitraryStorageUtils.getStorageSetting(accountIdentifier, key, conversationToken); - if (valueFromDb == null) { - return defaultVal; - } else { - return Integer.parseInt(valueFromDb.getValue()); - } - } - - @Override - public Set getStringSet(String key, Set defaultVal) { - return null; - } - - @Override - public void onSaveInstanceState(Bundle outState) { - - } - - @Override - public void onRestoreInstanceState(Bundle savedState) { - - } -} diff --git a/app/src/main/java/com/nextcloud/talk/utils/preferences/preferencestorage/DatabaseStorageModule.kt b/app/src/main/java/com/nextcloud/talk/utils/preferences/preferencestorage/DatabaseStorageModule.kt new file mode 100644 index 000000000..50db69a59 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/preferences/preferencestorage/DatabaseStorageModule.kt @@ -0,0 +1,280 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * Copyright (C) 2017-2018 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.utils.preferences.preferencestorage + +import android.os.Bundle +import android.text.TextUtils +import autodagger.AutoInjector +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.interfaces.ConversationInfoInterface +import com.nextcloud.talk.models.database.ArbitraryStorageEntity +import com.nextcloud.talk.models.json.generic.GenericOverall +import com.nextcloud.talk.newarch.local.models.UserNgEntity +import com.nextcloud.talk.newarch.local.models.hasSpreedFeatureCapability +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.database.arbitrarystorage.ArbitraryStorageUtils +import com.yarolegovich.mp.io.StorageModule +import io.reactivex.Observer +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class DatabaseStorageModule( + private val conversationUser: UserNgEntity, + private val conversationToken: String, + private val conversationInfoInterface: ConversationInfoInterface +) : StorageModule { + @JvmField + @Inject + var arbitraryStorageUtils: ArbitraryStorageUtils? = + null + @JvmField + @Inject + var ncApi: NcApi? = null + private val accountIdentifier: Long + private var lobbyValue = false + private var favoriteConversationValue = false + private var allowGuestsValue = false + private var hasPassword: Boolean? = null + private var conversationNameValue: String? = null + private var messageNotificationLevel: String? = null + override fun saveBoolean( + key: String, + value: Boolean + ) { + if (key != "conversation_lobby" && key != "allow_guests" && key != "favorite_conversation" + ) { + arbitraryStorageUtils!!.storeStorageSetting( + accountIdentifier, key, value.toString(), + conversationToken + ) + } else { + when (key) { + "conversation_lobby" -> lobbyValue = value + "allow_guests" -> allowGuestsValue = value + "favorite_conversation" -> favoriteConversationValue = value + else -> { + } + } + } + } + + override fun saveString( + key: String, + value: String + ) { + if (key != "message_notification_level" + && key != "conversation_name" + && key != "conversation_password" + ) { + arbitraryStorageUtils!!.storeStorageSetting(accountIdentifier, key, value, conversationToken) + } else { + if (key == "message_notification_level") { + if (conversationUser.hasSpreedFeatureCapability("notification-levels")) { + if (!TextUtils.isEmpty( + messageNotificationLevel + ) && messageNotificationLevel != value + ) { + val intValue: Int + intValue = when (value) { + "never" -> 3 + "mention" -> 2 + "always" -> 1 + else -> 0 + } + ncApi!!.setNotificationLevel( + ApiUtils.getCredentials( + conversationUser.username, + conversationUser.token + ), + ApiUtils.getUrlForSettingNotificationlevel( + conversationUser.baseUrl, + conversationToken + ), + intValue + ) + .subscribeOn(Schedulers.io()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) {} + override fun onNext(genericOverall: GenericOverall) { + messageNotificationLevel = value + } + + override fun onError(e: Throwable) {} + override fun onComplete() {} + }) + } else { + messageNotificationLevel = value + } + } + } else if (key == "conversation_password") { + if (hasPassword != null) { + ncApi!!.setPassword( + ApiUtils.getCredentials( + conversationUser.username, + conversationUser.token + ), + ApiUtils.getUrlForPassword( + conversationUser.baseUrl, + conversationToken + ), value + ) + .subscribeOn(Schedulers.io()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) {} + override fun onNext(genericOverall: GenericOverall) { + hasPassword = !TextUtils.isEmpty(value) + conversationInfoInterface.passwordSet(TextUtils.isEmpty(value)) + } + + override fun onError(e: Throwable) {} + override fun onComplete() {} + }) + } else { + hasPassword = value.toBoolean() + } + } else if (key == "conversation_name") { + if (!TextUtils.isEmpty( + conversationNameValue + ) && conversationNameValue != value + ) { + ncApi!!.renameRoom( + ApiUtils.getCredentials( + conversationUser.username, + conversationUser.token + ), ApiUtils.getRoom( + conversationUser.baseUrl, + conversationToken + ), value + ) + .subscribeOn(Schedulers.io()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) {} + override fun onNext(genericOverall: GenericOverall) { + conversationNameValue = value + conversationInfoInterface.conversationNameSet(value) + } + + override fun onError(e: Throwable) {} + override fun onComplete() {} + }) + } else { + conversationNameValue = value + } + } + } + } + + override fun saveInt( + key: String, + value: Int + ) { + arbitraryStorageUtils!!.storeStorageSetting( + accountIdentifier, key, Integer.toString(value), + conversationToken + ) + } + + override fun saveStringSet( + key: String, + value: Set + ) { + } + + override fun getBoolean( + key: String, + defaultVal: Boolean + ): Boolean { + return if (key == "conversation_lobby") { + lobbyValue + } else if (key == "allow_guests") { + allowGuestsValue + } else if (key == "favorite_conversation") { + favoriteConversationValue + } else { + val valueFromDb: ArbitraryStorageEntity? = + arbitraryStorageUtils!!.getStorageSetting(accountIdentifier, key, conversationToken) + if (valueFromDb == null) { + defaultVal + } else { + valueFromDb.value!!.toBoolean() + } + } + } + + override fun getString( + key: String, + defaultVal: String + ): String { + if (key != "message_notification_level" + && key != "conversation_name" + && key != "conversation_password" + ) { + val valueFromDb: ArbitraryStorageEntity? = + arbitraryStorageUtils!!.getStorageSetting(accountIdentifier, key, conversationToken) + return if (valueFromDb == null) { + defaultVal + } else { + valueFromDb.value + } + } else if (key == "message_notification_level") { + return messageNotificationLevel!! + } else if (key == "conversation_name") { + return conversationNameValue!! + } else if (key == "conversation_password") { + return "" + } + return "" + } + + override fun getInt( + key: String, + defaultVal: Int + ): Int { + val valueFromDb: ArbitraryStorageEntity? = + arbitraryStorageUtils!!.getStorageSetting(accountIdentifier, key, conversationToken) + return if (valueFromDb == null) { + defaultVal + } else { + Integer.parseInt(valueFromDb.value) + } + } + + override fun getStringSet( + key: String, + defaultVal: Set + ): Set? { + return null + } + + override fun onSaveInstanceState(outState: Bundle) {} + override fun onRestoreInstanceState(savedState: Bundle) {} + + init { + sharedApplication!! + .componentApplication + .inject(this) + accountIdentifier = conversationUser.id + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/utils/singletons/ApplicationWideCurrentRoomHolder.java b/app/src/main/java/com/nextcloud/talk/utils/singletons/ApplicationWideCurrentRoomHolder.java deleted file mode 100644 index e1f7563b3..000000000 --- a/app/src/main/java/com/nextcloud/talk/utils/singletons/ApplicationWideCurrentRoomHolder.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Nextcloud Talk application - * - * @author Mario Danic - * Copyright (C) 2017-2018 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.utils.singletons; - -import com.nextcloud.talk.models.database.UserEntity; - -public class ApplicationWideCurrentRoomHolder { - private static final ApplicationWideCurrentRoomHolder holder = - new ApplicationWideCurrentRoomHolder(); - private String currentRoomId = ""; - private String currentRoomToken = ""; - private UserEntity userInRoom = new UserEntity(); - private boolean inCall = false; - private String session = ""; - - public static ApplicationWideCurrentRoomHolder getInstance() { - return holder; - } - - public void clear() { - currentRoomId = ""; - userInRoom = new UserEntity(); - inCall = false; - currentRoomToken = ""; - session = ""; - } - - public String getCurrentRoomToken() { - return currentRoomToken; - } - - public void setCurrentRoomToken(String currentRoomToken) { - this.currentRoomToken = currentRoomToken; - } - - public String getCurrentRoomId() { - return currentRoomId; - } - - public void setCurrentRoomId(String currentRoomId) { - this.currentRoomId = currentRoomId; - } - - public UserEntity getUserInRoom() { - return userInRoom; - } - - public void setUserInRoom(UserEntity userInRoom) { - this.userInRoom = userInRoom; - } - - public boolean isInCall() { - return inCall; - } - - public void setInCall(boolean inCall) { - this.inCall = inCall; - } - - public String getSession() { - return session; - } - - public void setSession(String session) { - this.session = session; - } -} diff --git a/app/src/main/java/com/nextcloud/talk/webrtc/MagicWebSocketInstance.java b/app/src/main/java/com/nextcloud/talk/webrtc/MagicWebSocketInstance.java index bfcc0e709..5baec10b6 100644 --- a/app/src/main/java/com/nextcloud/talk/webrtc/MagicWebSocketInstance.java +++ b/app/src/main/java/com/nextcloud/talk/webrtc/MagicWebSocketInstance.java @@ -40,6 +40,7 @@ import com.nextcloud.talk.models.json.websocket.ErrorOverallWebSocketMessage; import com.nextcloud.talk.models.json.websocket.EventOverallWebSocketMessage; import com.nextcloud.talk.models.json.websocket.HelloResponseOverallWebSocketMessage; import com.nextcloud.talk.models.json.websocket.JoinedRoomOverallWebSocketMessage; +import com.nextcloud.talk.newarch.local.models.UserNgEntity; import com.nextcloud.talk.utils.LoggingUtils; import com.nextcloud.talk.utils.MagicMap; import com.nextcloud.talk.utils.bundle.BundleKeys; @@ -72,7 +73,7 @@ public class MagicWebSocketInstance extends WebSocketListener { @Inject Context context; - private UserEntity conversationUser; + private UserNgEntity conversationUser; private String webSocketTicket; private String resumeId; private String sessionId; @@ -91,7 +92,7 @@ public class MagicWebSocketInstance extends WebSocketListener { private List messagesQueue = new ArrayList<>(); - MagicWebSocketInstance(UserEntity conversationUser, String connectionUrl, + MagicWebSocketInstance(UserNgEntity conversationUser, String connectionUrl, String webSocketTicket) { NextcloudTalkApplication.Companion.getSharedApplication() .getComponentApplication() diff --git a/app/src/main/java/com/nextcloud/talk/webrtc/WebSocketConnectionHelper.java b/app/src/main/java/com/nextcloud/talk/webrtc/WebSocketConnectionHelper.java index cf901ff0b..cad7cfb85 100644 --- a/app/src/main/java/com/nextcloud/talk/webrtc/WebSocketConnectionHelper.java +++ b/app/src/main/java/com/nextcloud/talk/webrtc/WebSocketConnectionHelper.java @@ -36,6 +36,7 @@ import com.nextcloud.talk.models.json.websocket.RequestOfferSignalingMessage; import com.nextcloud.talk.models.json.websocket.RoomOverallWebSocketMessage; import com.nextcloud.talk.models.json.websocket.RoomWebSocketMessage; import com.nextcloud.talk.models.json.websocket.SignalingDataWebSocketMessageForOffer; +import com.nextcloud.talk.newarch.local.models.UserNgEntity; import com.nextcloud.talk.utils.ApiUtils; import java.util.HashMap; import java.util.Map; @@ -65,7 +66,7 @@ public class WebSocketConnectionHelper { } public static synchronized MagicWebSocketInstance getExternalSignalingInstanceForServer( - String url, UserEntity userEntity, String webSocketTicket, boolean isGuest) { + String url, UserNgEntity userEntity, String webSocketTicket, boolean isGuest) { String generatedURL = url.replace("https://", "wss://").replace("http://", "ws://"); if (generatedURL.endsWith("/")) { @@ -102,7 +103,7 @@ public class WebSocketConnectionHelper { } } - HelloOverallWebSocketMessage getAssembledHelloModel(UserEntity userEntity, String ticket) { + HelloOverallWebSocketMessage getAssembledHelloModel(UserNgEntity userEntity, String ticket) { HelloOverallWebSocketMessage helloOverallWebSocketMessage = new HelloOverallWebSocketMessage(); helloOverallWebSocketMessage.setType("hello"); HelloWebSocketMessage helloWebSocketMessage = new HelloWebSocketMessage();