Significant changes

Signed-off-by: Mario Danic <mario@lovelyhq.com>
This commit is contained in:
Mario Danic 2019-12-08 11:23:57 +01:00
parent d5cf261776
commit 8eb2f78a23
No known key found for this signature in database
GPG Key ID: CDE0BBD2738C4CC0
35 changed files with 1203 additions and 1143 deletions

View File

@ -2,17 +2,17 @@
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "c81b4edb8abdd29b77836d16a7d991c2",
"identityHash": "c7b1b47572d7ace1b422d0a3887a54e1",
"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` 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)",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `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, PRIMARY KEY(`id`), FOREIGN KEY(`user`) REFERENCES `users`(`id`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "user",
@ -169,7 +169,7 @@
"columnNames": [
"id"
],
"autoGenerate": true
"autoGenerate": false
},
"indices": [
{
@ -198,19 +198,19 @@
},
{
"tableName": "messages",
"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)",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `conversation` TEXT NOT NULL, `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, PRIMARY KEY(`id`), FOREIGN KEY(`conversation`) REFERENCES `conversations`(`id`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "conversation",
"columnName": "conversation",
"affinity": "INTEGER",
"notNull": false
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "messageId",
@ -259,7 +259,7 @@
"columnNames": [
"id"
],
"autoGenerate": true
"autoGenerate": false
},
"indices": [
{
@ -369,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, 'c81b4edb8abdd29b77836d16a7d991c2')"
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c7b1b47572d7ace1b422d0a3887a54e1')"
]
}
}

View File

@ -118,6 +118,7 @@ import org.apache.commons.lang3.StringEscapeUtils
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import org.koin.android.ext.android.inject
import org.parceler.Parcel
import org.webrtc.AudioSource
import org.webrtc.AudioTrack
@ -194,10 +195,8 @@ class CallController(args: Bundle) : BaseController() {
@JvmField
@Inject
var userUtils: UserUtils? = null
@JvmField
@Inject
var cookieManager: CookieManager? = null
val cookieManager: CookieManager by inject()
private var peerConnectionFactory: PeerConnectionFactory? = null
private var audioConstraints: MediaConstraints? = null
private var videoConstraints: MediaConstraints? = null
@ -214,7 +213,7 @@ class CallController(args: Bundle) : BaseController() {
private var pingDisposable: Disposable? = null
private var iceServers: MutableList<PeerConnection.IceServer>? = null
private var cameraEnumerator: CameraEnumerator? = null
private var roomToken: String? = null
private var roomToken: String
private val conversationUser: UserNgEntity?
private var callSession: String? = null
private var localMediaStream: MediaStream? = null
@ -416,7 +415,7 @@ class CallController(args: Bundle) : BaseController() {
override fun onNext(roomsOverall: RoomsOverall) {
for (conversation in roomsOverall.ocs.data) {
if (roomId == conversation.conversationId) {
roomToken = conversation.token
roomToken = conversation.token.toString()
break
}
}
@ -1321,9 +1320,11 @@ class CallController(args: Bundle) : BaseController() {
Integer.valueOf(webSocketCommunicationEvent.hashMap!!["jobId"]!!)
) as NCSignalingMessage
)
"peerReadyForRequestingOffer" -> webSocketClient!!.requestOfferForSessionIdWithType(
webSocketCommunicationEvent.hashMap!!["sessionId"], "video"
)
"peerReadyForRequestingOffer" -> webSocketCommunicationEvent.hashMap!!["sessionId"]?.let {
webSocketClient!!.requestOfferForSessionIdWithType(
it, "video"
)
}
}
}
@ -2166,7 +2167,7 @@ class CallController(args: Bundle) : BaseController() {
var sessionOrUserId = sessionOrUserId
if (isFromAnEvent && hasExternalSignalingServer) {
// get session based on userId
sessionOrUserId = webSocketClient!!.getSessionForUserId(sessionOrUserId)
sessionOrUserId = webSocketClient!!.getSessionForUserId(sessionOrUserId).toString()
}
sessionOrUserId += "+$type"

View File

@ -852,8 +852,8 @@ class ChatController(args: Bundle) : BaseController(), MessagesListAdapter
}
if (magicWebSocketInstance != null) {
magicWebSocketInstance?.joinRoomWithRoomTokenAndSession(
roomToken, currentConversation?.sessionId
magicWebSocketInstance?.joinRoomWithRoomTokenAndSession(roomToken!!,
currentConversation?.sessionId
)
}
if (startCallFromNotification != null && startCallFromNotification == true) {
@ -874,7 +874,7 @@ class ChatController(args: Bundle) : BaseController(), MessagesListAdapter
inConversation = true
if (magicWebSocketInstance != null) {
magicWebSocketInstance?.joinRoomWithRoomTokenAndSession(
roomToken,
roomToken!!,
currentConversation?.sessionId
)
}

View File

@ -61,7 +61,6 @@ import com.nextcloud.talk.events.EventStatus
import com.nextcloud.talk.interfaces.ConversationInfoInterface
import com.nextcloud.talk.jobs.DeleteConversationWorker
import com.nextcloud.talk.jobs.LeaveConversationWorker
import com.nextcloud.talk.models.database.UserEntity
import com.nextcloud.talk.models.json.conversations.Conversation
import com.nextcloud.talk.models.json.conversations.Conversation.ConversationType
import com.nextcloud.talk.models.json.conversations.Conversation.ConversationType.GROUP_CONVERSATION
@ -75,8 +74,6 @@ 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
import com.nextcloud.talk.utils.DisplayUtils

View File

@ -1,486 +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 <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.controllers;
import android.annotation.SuppressLint;
import android.content.pm.ActivityInfo;
import android.graphics.Bitmap;
import android.net.http.SslCertificate;
import android.net.http.SslError;
import android.os.Build;
import android.os.Bundle;
import android.security.KeyChain;
import android.security.KeyChainException;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.ClientCertRequest;
import android.webkit.CookieSyncManager;
import android.webkit.SslErrorHandler;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.ProgressBar;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkManager;
import autodagger.AutoInjector;
import butterknife.BindView;
import com.bluelinelabs.conductor.RouterTransaction;
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler;
import com.nextcloud.talk.R;
import com.nextcloud.talk.application.NextcloudTalkApplication;
import com.nextcloud.talk.controllers.base.BaseController;
import com.nextcloud.talk.events.CertificateEvent;
import com.nextcloud.talk.jobs.PushRegistrationWorker;
import com.nextcloud.talk.models.LoginData;
import com.nextcloud.talk.models.database.UserEntity;
import com.nextcloud.talk.utils.bundle.BundleKeys;
import com.nextcloud.talk.utils.database.user.UserUtils;
import com.nextcloud.talk.utils.preferences.AppPreferences;
import com.nextcloud.talk.utils.singletons.ApplicationWideMessageHolder;
import com.nextcloud.talk.utils.ssl.MagicTrustManager;
import com.uber.autodispose.AutoDispose;
import de.cotech.hw.fido.WebViewFidoBridge;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import io.requery.Persistable;
import io.requery.reactivex.ReactiveEntityStore;
import java.lang.reflect.Field;
import java.net.CookieManager;
import java.net.URLDecoder;
import java.security.PrivateKey;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import javax.inject.Inject;
import org.greenrobot.eventbus.EventBus;
@AutoInjector(NextcloudTalkApplication.class)
public class WebViewLoginController extends BaseController {
public static final String TAG = "WebViewLoginController";
private final String PROTOCOL_SUFFIX = "://";
private final String LOGIN_URL_DATA_KEY_VALUE_SEPARATOR = ":";
@Inject
UserUtils userUtils;
@Inject
AppPreferences appPreferences;
@Inject
ReactiveEntityStore<Persistable> dataStore;
@Inject
MagicTrustManager magicTrustManager;
@Inject
EventBus eventBus;
@Inject
CookieManager cookieManager;
@BindView(R.id.webview)
WebView webView;
@BindView(R.id.progress_bar)
ProgressBar progressBar;
private String assembledPrefix;
private Disposable userQueryDisposable;
private String baseUrl;
private boolean isPasswordUpdate;
private String username;
private String password;
private int loginStep = 0;
private boolean automatedLoginAttempted = false;
private WebViewFidoBridge webViewFidoBridge;
public WebViewLoginController() {
}
public WebViewLoginController(String baseUrl, boolean isPasswordUpdate) {
this.baseUrl = baseUrl;
this.isPasswordUpdate = isPasswordUpdate;
}
public WebViewLoginController(String baseUrl, boolean isPasswordUpdate, String username,
String password) {
this.baseUrl = baseUrl;
this.isPasswordUpdate = isPasswordUpdate;
this.username = username;
this.password = password;
}
private String getWebLoginUserAgent() {
return Build.MANUFACTURER.substring(0, 1).toUpperCase(Locale.getDefault()) +
Build.MANUFACTURER.substring(1).toLowerCase(Locale.getDefault()) + " " + Build.MODEL + " ("
+ getResources().getString(R.string.nc_app_name) + ")";
}
@Override
protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
return inflater.inflate(R.layout.controller_web_view_login, container, false);
}
@SuppressLint("SetJavaScriptEnabled")
@Override
protected void onViewBound(@NonNull View view) {
super.onViewBound(view);
NextcloudTalkApplication.Companion.getSharedApplication()
.getComponentApplication()
.inject(this);
if (getActivity() != null) {
getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
if (getActionBar() != null) {
getActionBar().hide();
}
assembledPrefix =
getResources().getString(R.string.nc_talk_login_scheme) + PROTOCOL_SUFFIX + "login/";
webView.getSettings().setAllowFileAccess(false);
webView.getSettings().setAllowFileAccessFromFileURLs(false);
webView.getSettings().setJavaScriptEnabled(true);
webView.getSettings().setJavaScriptCanOpenWindowsAutomatically(false);
webView.getSettings().setDomStorageEnabled(true);
webView.getSettings().setUserAgentString(getWebLoginUserAgent());
webView.getSettings().setSaveFormData(false);
webView.getSettings().setSavePassword(false);
webView.getSettings().setRenderPriority(WebSettings.RenderPriority.HIGH);
webView.clearCache(true);
webView.clearFormData();
webView.clearHistory();
WebView.clearClientCertPreferences(null);
webViewFidoBridge =
WebViewFidoBridge.createInstanceForWebView((AppCompatActivity) getActivity(), webView);
CookieSyncManager.createInstance(getActivity());
android.webkit.CookieManager.getInstance().removeAllCookies(null);
Map<String, String> headers = new HashMap<>();
headers.put("OCS-APIRequest", "true");
webView.setWebViewClient(new WebViewClient() {
private boolean basePageLoaded;
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
webViewFidoBridge.delegateShouldInterceptRequest(view, request);
return super.shouldInterceptRequest(view, request);
}
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
webViewFidoBridge.delegateOnPageStarted(view, url, favicon);
}
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url.startsWith(assembledPrefix)) {
parseAndLoginFromWebView(url);
return true;
}
return false;
}
@Override
public void onPageFinished(WebView view, String url) {
loginStep++;
if (!basePageLoaded) {
if (progressBar != null) {
progressBar.setVisibility(View.GONE);
}
if (webView != null) {
webView.setVisibility(View.VISIBLE);
}
basePageLoaded = true;
}
if (!TextUtils.isEmpty(username) && webView != null) {
if (loginStep == 1) {
webView.loadUrl("javascript: {document.getElementsByClassName('login')[0].click(); };");
} else if (!automatedLoginAttempted) {
automatedLoginAttempted = true;
if (TextUtils.isEmpty(password)) {
webView.loadUrl("javascript:var justStore = document.getElementById('user').value = '"
+ username
+ "';");
} else {
webView.loadUrl("javascript: {" +
"document.getElementById('user').value = '" + username + "';" +
"document.getElementById('password').value = '" + password + "';" +
"document.getElementById('submit').click(); };");
}
}
}
super.onPageFinished(view, url);
}
@Override
public void onReceivedClientCertRequest(WebView view, ClientCertRequest request) {
UserEntity userEntity = userUtils.getCurrentUser();
String alias = null;
if (!isPasswordUpdate) {
alias = appPreferences.getTemporaryClientCertAlias();
}
if (TextUtils.isEmpty(alias) && (userEntity != null)) {
alias = userEntity.getClientCertificate();
}
if (!TextUtils.isEmpty(alias)) {
String finalAlias = alias;
new Thread(() -> {
try {
PrivateKey privateKey = KeyChain.getPrivateKey(getActivity(), finalAlias);
X509Certificate[] certificates =
KeyChain.getCertificateChain(getActivity(), finalAlias);
if (privateKey != null && certificates != null) {
request.proceed(privateKey, certificates);
} else {
request.cancel();
}
} catch (KeyChainException | InterruptedException e) {
request.cancel();
}
}).start();
} else {
KeyChain.choosePrivateKeyAlias(getActivity(), chosenAlias -> {
if (chosenAlias != null) {
appPreferences.setTemporaryClientCertAlias(chosenAlias);
new Thread(() -> {
PrivateKey privateKey = null;
try {
privateKey = KeyChain.getPrivateKey(getActivity(), chosenAlias);
X509Certificate[] certificates =
KeyChain.getCertificateChain(getActivity(), chosenAlias);
if (privateKey != null && certificates != null) {
request.proceed(privateKey, certificates);
} else {
request.cancel();
}
} catch (KeyChainException | InterruptedException e) {
request.cancel();
}
}).start();
} else {
request.cancel();
}
}, new String[] { "RSA", "EC" }, null, request.getHost(), request.getPort(), null);
}
}
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
try {
SslCertificate sslCertificate = error.getCertificate();
Field f = sslCertificate.getClass().getDeclaredField("mX509Certificate");
f.setAccessible(true);
X509Certificate cert = (X509Certificate) f.get(sslCertificate);
if (cert == null) {
handler.cancel();
} else {
try {
magicTrustManager.checkServerTrusted(new X509Certificate[] { cert }, "generic");
handler.proceed();
} catch (CertificateException exception) {
eventBus.post(new CertificateEvent(cert, magicTrustManager, handler));
}
}
} catch (Exception exception) {
handler.cancel();
}
}
@Override
public void onReceivedError(WebView view, int errorCode, String description,
String failingUrl) {
super.onReceivedError(view, errorCode, description, failingUrl);
}
});
webView.loadUrl(baseUrl + "/index.php/login/flow", headers);
}
private void dispose() {
if (userQueryDisposable != null && !userQueryDisposable.isDisposed()) {
userQueryDisposable.dispose();
}
userQueryDisposable = null;
}
private void parseAndLoginFromWebView(String dataString) {
LoginData loginData = parseLoginData(assembledPrefix, dataString);
if (loginData != null) {
dispose();
UserEntity currentUser = userUtils.getCurrentUser();
ApplicationWideMessageHolder.MessageType messageType = null;
if (!isPasswordUpdate && userUtils.getIfUserWithUsernameAndServer(loginData.getUsername(),
baseUrl)) {
messageType = ApplicationWideMessageHolder.MessageType.ACCOUNT_UPDATED_NOT_ADDED;
}
if (userUtils.checkIfUserIsScheduledForDeletion(loginData.getUsername(), baseUrl)) {
ApplicationWideMessageHolder.getInstance().setMessageType(
ApplicationWideMessageHolder.MessageType.ACCOUNT_SCHEDULED_FOR_DELETION);
if (!isPasswordUpdate) {
getRouter().popToRoot();
} else {
getRouter().popCurrentController();
}
}
ApplicationWideMessageHolder.MessageType finalMessageType = messageType;
cookieManager.getCookieStore().removeAll();
if (!isPasswordUpdate && finalMessageType == null) {
Bundle bundle = new Bundle();
bundle.putString(BundleKeys.INSTANCE.getKEY_USERNAME(), loginData.getUsername());
bundle.putString(BundleKeys.INSTANCE.getKEY_TOKEN(), loginData.getToken());
bundle.putString(BundleKeys.INSTANCE.getKEY_BASE_URL(), loginData.getServerUrl());
String protocol = "";
if (baseUrl.startsWith("http://")) {
protocol = "http://";
} else if (baseUrl.startsWith("https://")) {
protocol = "https://";
}
if (!TextUtils.isEmpty(protocol)) {
bundle.putString(BundleKeys.INSTANCE.getKEY_ORIGINAL_PROTOCOL(), protocol);
}
getRouter().pushController(RouterTransaction.with(new AccountVerificationController
(bundle)).pushChangeHandler(new HorizontalChangeHandler())
.popChangeHandler(new HorizontalChangeHandler()));
} else {
if (isPasswordUpdate) {
if (currentUser != null) {
userQueryDisposable = userUtils.createOrUpdateUser(null, loginData.getToken(),
null, null, "", true,
null, currentUser.getId(), null, appPreferences.getTemporaryClientCertAlias(), null)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.as(AutoDispose.autoDisposable(getScopeProvider()))
.subscribe(userEntity -> {
if (finalMessageType != null) {
ApplicationWideMessageHolder.getInstance().setMessageType(finalMessageType);
}
OneTimeWorkRequest pushRegistrationWork =
new OneTimeWorkRequest.Builder(PushRegistrationWorker.class).build();
WorkManager.getInstance().enqueue(pushRegistrationWork);
getRouter().popCurrentController();
}, throwable -> dispose(),
this::dispose);
}
} else {
if (finalMessageType != null) {
ApplicationWideMessageHolder.getInstance().setMessageType(finalMessageType);
}
getRouter().popToRoot();
}
}
}
}
private LoginData parseLoginData(String prefix, String dataString) {
if (dataString.length() < prefix.length()) {
return null;
}
LoginData loginData = new LoginData();
// format is xxx://login/server:xxx&user:xxx&password:xxx
String data = dataString.substring(prefix.length());
String[] values = data.split("&");
if (values.length != 3) {
return null;
}
for (String value : values) {
if (value.startsWith("user" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) {
loginData.setUsername(URLDecoder.decode(
value.substring(("user" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length())));
} else if (value.startsWith("password" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) {
loginData.setToken(URLDecoder.decode(
value.substring(("password" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length())));
} else if (value.startsWith("server" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR)) {
loginData.setServerUrl(URLDecoder.decode(
value.substring(("server" + LOGIN_URL_DATA_KEY_VALUE_SEPARATOR).length())));
} else {
return null;
}
}
if (!TextUtils.isEmpty(loginData.getServerUrl())
&& !TextUtils.isEmpty(loginData.getUsername())
&&
!TextUtils.isEmpty(loginData.getToken())) {
return loginData;
} else {
return null;
}
}
@Override
public void onDestroy() {
super.onDestroy();
dispose();
}
@Override
protected void onDestroyView(@NonNull View view) {
super.onDestroyView(view);
if (getActivity() != null) {
getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR);
}
}
}

View File

@ -0,0 +1,462 @@
/*
*
* 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 <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.controllers
import android.annotation.SuppressLint
import android.content.pm.ActivityInfo
import android.net.http.SslError
import android.os.Build
import android.os.Bundle
import android.security.KeyChain
import android.security.KeyChainException
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.webkit.ClientCertRequest
import android.webkit.CookieSyncManager
import android.webkit.SslErrorHandler
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebSettings.RenderPriority.HIGH
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.appcompat.app.AppCompatActivity
import androidx.work.OneTimeWorkRequest.Builder
import androidx.work.WorkManager
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
import com.nextcloud.talk.R.layout
import com.nextcloud.talk.R.string
import com.nextcloud.talk.controllers.base.BaseController
import com.nextcloud.talk.events.CertificateEvent
import com.nextcloud.talk.jobs.PushRegistrationWorker
import com.nextcloud.talk.models.LoginData
import com.nextcloud.talk.newarch.domain.repository.offline.UsersRepository
import com.nextcloud.talk.newarch.local.models.other.UserStatus
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_BASE_URL
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ORIGINAL_PROTOCOL
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_TOKEN
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USERNAME
import com.nextcloud.talk.utils.singletons.ApplicationWideMessageHolder
import com.nextcloud.talk.utils.singletons.ApplicationWideMessageHolder.MessageType
import com.nextcloud.talk.utils.singletons.ApplicationWideMessageHolder.MessageType.ACCOUNT_SCHEDULED_FOR_DELETION
import com.nextcloud.talk.utils.singletons.ApplicationWideMessageHolder.MessageType.ACCOUNT_UPDATED_NOT_ADDED
import com.nextcloud.talk.utils.ssl.MagicTrustManager
import de.cotech.hw.fido.WebViewFidoBridge
import kotlinx.android.synthetic.main.controller_web_view_login.view.progress_bar
import kotlinx.android.synthetic.main.controller_web_view_login.view.webview
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.android.ext.android.inject
import java.net.CookieManager
import java.net.URLDecoder
import java.security.PrivateKey
import java.security.cert.CertificateException
import java.security.cert.X509Certificate
import java.util.Locale
class WebViewLoginController : BaseController {
private val PROTOCOL_SUFFIX = "://"
private val LOGIN_URL_DATA_KEY_VALUE_SEPARATOR = ":"
val magicTrustManager: MagicTrustManager by inject()
val cookieManager: CookieManager by inject()
val usersRepository: UsersRepository by inject()
private var assembledPrefix: String? = null
private var baseUrl: String? = null
private var isPasswordUpdate = false
private var username: String? = null
private var password: String? = null
private var loginStep = 0
private var automatedLoginAttempted = false
private var webViewFidoBridge: WebViewFidoBridge? = null
constructor(bundle: Bundle)
constructor(
baseUrl: String?,
isPasswordUpdate: Boolean
) {
this.baseUrl = baseUrl
this.isPasswordUpdate = isPasswordUpdate
}
constructor(
baseUrl: String?,
isPasswordUpdate: Boolean,
username: String?,
password: String?
) {
this.baseUrl = baseUrl
this.isPasswordUpdate = isPasswordUpdate
this.username = username
this.password = password
}
private val webLoginUserAgent: String
private get() = (Build.MANUFACTURER.substring(0, 1).toUpperCase(
Locale.getDefault()
) +
Build.MANUFACTURER.substring(1).toLowerCase(
Locale.getDefault()
) + " " + Build.MODEL + " ("
+ resources!!.getString(string.nc_app_name) + ")")
override fun inflateView(
inflater: LayoutInflater,
container: ViewGroup
): View {
return inflater.inflate(layout.controller_web_view_login, container, false)
}
@SuppressLint("SetJavaScriptEnabled")
override fun onViewBound(view: View) {
super.onViewBound(view)
if (activity != null) {
activity!!.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
if (actionBar != null) {
actionBar!!.hide()
}
assembledPrefix =
resources!!.getString(string.nc_talk_login_scheme) + PROTOCOL_SUFFIX + "login/"
view.webview.apply {
settings.allowFileAccess = false
settings.allowFileAccessFromFileURLs = false
settings.javaScriptEnabled = true
settings.javaScriptCanOpenWindowsAutomatically = false
settings.domStorageEnabled = true
settings.userAgentString = webLoginUserAgent
settings.saveFormData = false
settings.savePassword = false
settings.setRenderPriority(HIGH)
clearCache(true)
clearFormData()
clearHistory()
clearSslPreferences()
}
WebView.clearClientCertPreferences(null)
webViewFidoBridge =
WebViewFidoBridge.createInstanceForWebView(activity as AppCompatActivity?, view.webview)
CookieSyncManager.createInstance(activity)
android.webkit.CookieManager.getInstance()
.removeAllCookies(null)
val headers: MutableMap<String, String> = hashMapOf()
headers["OCS-APIRequest"] = "true"
view.webview.webViewClient = object : WebViewClient() {
private var basePageLoaded = false
override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest
): WebResourceResponse? {
webViewFidoBridge?.delegateShouldInterceptRequest(view, request)
return super.shouldInterceptRequest(view, request)
}
override fun shouldOverrideUrlLoading(
view: WebView,
url: String
): Boolean {
if (url.startsWith(assembledPrefix!!)) {
parseAndLoginFromWebView(url)
return true
}
return false
}
override fun onPageFinished(
view: WebView,
url: String
) {
loginStep++
if (!basePageLoaded) {
if (view.progress_bar != null) {
view.progress_bar!!.visibility = View.GONE
}
if (view.webview != null) {
view.webview.visibility = View.VISIBLE
}
basePageLoaded = true
}
if (!TextUtils.isEmpty(username)) {
if (loginStep == 1) {
view.webview.loadUrl(
"javascript: {document.getElementsByClassName('login')[0].click(); };"
)
} else if (!automatedLoginAttempted) {
automatedLoginAttempted = true
if (TextUtils.isEmpty(password)) {
view.webview.loadUrl(
"javascript:var justStore = document.getElementById('user').value = '"
+ username
+ "';"
)
} else {
view.webview.loadUrl(
"javascript: {" +
"document.getElementById('user').value = '" + username + "';" +
"document.getElementById('password').value = '" + password + "';" +
"document.getElementById('submit').click(); };"
)
}
}
}
super.onPageFinished(view, url)
}
override fun onReceivedClientCertRequest(
view: WebView,
request: ClientCertRequest
) {
val userEntity = usersRepository.getActiveUser()
var alias: String? = null
if (!isPasswordUpdate) {
alias = appPreferences.temporaryClientCertAlias
}
if (TextUtils.isEmpty(alias)) {
alias = userEntity!!.clientCertificate
}
if (!TextUtils.isEmpty(alias)) {
val finalAlias = alias
Thread(Runnable {
try {
val privateKey =
KeyChain.getPrivateKey(activity!!, finalAlias!!)
val certificates =
KeyChain.getCertificateChain(activity!!, finalAlias)
if (privateKey != null && certificates != null) {
request.proceed(privateKey, certificates)
} else {
request.cancel()
}
} catch (e: KeyChainException) {
request.cancel()
} catch (e: InterruptedException) {
request.cancel()
}
})
.start()
} else {
KeyChain.choosePrivateKeyAlias(
activity!!, { chosenAlias: String? ->
if (chosenAlias != null) {
appPreferences.temporaryClientCertAlias = chosenAlias
Thread(Runnable {
var privateKey: PrivateKey? = null
try {
privateKey = KeyChain.getPrivateKey(activity!!, chosenAlias)
val certificates =
KeyChain.getCertificateChain(activity!!, chosenAlias)
if (privateKey != null && certificates != null) {
request.proceed(privateKey, certificates)
} else {
request.cancel()
}
} catch (e: KeyChainException) {
request.cancel()
} catch (e: InterruptedException) {
request.cancel()
}
})
.start()
} else {
request.cancel()
}
}, arrayOf("RSA", "EC"), null, request.host, request.port, null
)
}
}
override fun onReceivedSslError(
view: WebView,
handler: SslErrorHandler,
error: SslError
) {
try {
val sslCertificate = error.certificate
val f =
sslCertificate.javaClass.getDeclaredField("mX509Certificate")
f.isAccessible = true
val cert =
f[sslCertificate] as X509Certificate
if (cert == null) {
handler.cancel()
} else {
try {
magicTrustManager.checkServerTrusted(
arrayOf(cert), "generic"
)
handler.proceed()
} catch (exception: CertificateException) {
eventBus.post(CertificateEvent(cert, magicTrustManager, handler))
}
}
} catch (exception: Exception) {
handler.cancel()
}
}
}
view.webview.loadUrl("$baseUrl/index.php/login/flow", headers)
}
private fun parseAndLoginFromWebView(dataString: String) {
val loginData = parseLoginData(assembledPrefix, dataString)
if (loginData != null) {
GlobalScope.launch {
val targetUser =
usersRepository.getUserWithUsernameAndServer(loginData.username!!, baseUrl!!)
var messageType: MessageType? = null
if (!isPasswordUpdate && targetUser != null) {
messageType = ACCOUNT_UPDATED_NOT_ADDED
}
if (targetUser != null && UserStatus.PENDING_DELETE.equals(targetUser.status)) {
ApplicationWideMessageHolder.getInstance().messageType = ACCOUNT_SCHEDULED_FOR_DELETION
if (!isPasswordUpdate) {
withContext(Dispatchers.Main) {
router.popToRoot()
}
} else {
withContext(Dispatchers.Main) {
router.popCurrentController()
}
}
}
val finalMessageType = messageType
cookieManager.cookieStore.removeAll()
if (!isPasswordUpdate && finalMessageType == null) {
val bundle = Bundle()
bundle.putString(KEY_USERNAME, loginData.username)
bundle.putString(KEY_TOKEN, loginData.token)
bundle.putString(KEY_BASE_URL, loginData.serverUrl)
var protocol = ""
if (baseUrl!!.startsWith("http://")) {
protocol = "http://"
} else if (baseUrl!!.startsWith("https://")) {
protocol = "https://"
}
if (!TextUtils.isEmpty(protocol)) {
bundle.putString(KEY_ORIGINAL_PROTOCOL, protocol)
}
withContext(Dispatchers.Main) {
router.pushController(
RouterTransaction.with(AccountVerificationController(bundle)).pushChangeHandler(
HorizontalChangeHandler()
)
.popChangeHandler(HorizontalChangeHandler())
)
}
} else {
if (isPasswordUpdate && targetUser != null) {
targetUser.token = loginData.token
val updatedRows = usersRepository.updateUser(targetUser)
if (updatedRows > 0) {
if (finalMessageType != null) {
ApplicationWideMessageHolder.getInstance().messageType = finalMessageType
}
val pushRegistrationWork = Builder(PushRegistrationWorker::class.java).build()
WorkManager.getInstance()
.enqueue(pushRegistrationWork)
withContext(Dispatchers.Main) {
router.popCurrentController()
}
} else {
// do nothing
}
} else {
if (finalMessageType != null) {
ApplicationWideMessageHolder.getInstance()
.messageType = finalMessageType
}
withContext(Dispatchers.Main) {
router.popToRoot()
}
}
}
}
}
}
private fun parseLoginData(
prefix: String?,
dataString: String
): LoginData? {
if (dataString.length < prefix!!.length) {
return null
}
val loginData = LoginData()
// format is xxx://login/server:xxx&user:xxx&password:xxx
val data = dataString.substring(prefix.length)
val values = data.split("&")
.toTypedArray()
if (values.size != 3) {
return null
}
for (value in values) {
if (value.startsWith("user$LOGIN_URL_DATA_KEY_VALUE_SEPARATOR")) {
loginData.username = URLDecoder.decode(
value.substring("user$LOGIN_URL_DATA_KEY_VALUE_SEPARATOR".length)
)
} else if (value.startsWith("password$LOGIN_URL_DATA_KEY_VALUE_SEPARATOR")) {
loginData.token = URLDecoder.decode(
value.substring("password$LOGIN_URL_DATA_KEY_VALUE_SEPARATOR".length)
)
} else if (value.startsWith("server$LOGIN_URL_DATA_KEY_VALUE_SEPARATOR")) {
loginData.serverUrl = URLDecoder.decode(
value.substring("server$LOGIN_URL_DATA_KEY_VALUE_SEPARATOR".length)
)
} else {
return null
}
}
return if (!TextUtils.isEmpty(loginData.serverUrl)
&& !TextUtils.isEmpty(loginData.username)
&&
!TextUtils.isEmpty(loginData.token)
) {
loginData
} else {
null
}
}
override fun onDestroyView(view: View) {
super.onDestroyView(view)
if (activity != null) {
activity!!.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR
}
}
companion object {
const val TAG = "WebViewLoginController"
}
}

View File

@ -29,4 +29,10 @@ public class WebSocketCommunicationEvent {
public final String type;
@Nullable
public final HashMap<String, String> hashMap;
public WebSocketCommunicationEvent(String type,
@Nullable HashMap<String, String> hashMap) {
this.type = type;
this.hashMap = hashMap;
}
}

View File

@ -1,105 +0,0 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.jobs;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.work.Data;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import autodagger.AutoInjector;
import com.nextcloud.talk.api.NcApi;
import com.nextcloud.talk.application.NextcloudTalkApplication;
import com.nextcloud.talk.models.database.UserEntity;
import com.nextcloud.talk.utils.ApiUtils;
import com.nextcloud.talk.utils.bundle.BundleKeys;
import com.nextcloud.talk.utils.database.user.UserUtils;
import io.reactivex.Observer;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject;
@AutoInjector(NextcloudTalkApplication.class)
public class ShareOperationWorker extends Worker {
@Inject
UserUtils userUtils;
@Inject
NcApi ncApi;
private long userId;
private UserEntity operationsUser;
private String roomToken;
private List<String> filesArray = new ArrayList<>();
private String credentials;
private String baseUrl;
public ShareOperationWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
NextcloudTalkApplication.Companion.getSharedApplication()
.getComponentApplication()
.inject(this);
Data data = workerParams.getInputData();
userId = data.getLong(BundleKeys.INSTANCE.getKEY_INTERNAL_USER_ID(), 0);
roomToken = data.getString(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN());
Collections.addAll(filesArray, data.getStringArray(BundleKeys.INSTANCE.getKEY_FILE_PATHS()));
operationsUser = userUtils.getUserWithId(userId);
credentials = ApiUtils.getCredentials(operationsUser.getUsername(), operationsUser.getToken());
baseUrl = operationsUser.getBaseUrl();
}
@NonNull
@Override
public Result doWork() {
for (int i = 0; i < filesArray.size(); i++) {
ncApi.createRemoteShare(credentials,
ApiUtils.getSharingUrl(baseUrl),
filesArray.get(i),
roomToken,
"10")
.subscribeOn(Schedulers.io())
.blockingSubscribe(new Observer<Void>() {
@Override
public void onSubscribe(Disposable d) {
}
@Override
public void onNext(Void aVoid) {
}
@Override
public void onError(Throwable e) {
}
@Override
public void onComplete() {
}
});
}
return Result.success();
}
}

View File

@ -0,0 +1,96 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.jobs
import android.content.Context
import androidx.work.Worker
import androidx.work.WorkerParameters
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.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.utils.ApiUtils
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FILE_PATHS
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
import io.reactivex.Observer
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import org.koin.core.KoinComponent
import org.koin.core.inject
import java.util.Collections
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
class ShareOperationWorker(
context: Context,
workerParams: WorkerParameters
) : Worker(context, workerParams), KoinComponent {
@JvmField @Inject
var ncApi: NcApi? = null
val usersRepository: UsersRepository by inject()
private val userId: Long
private val operationsUser: UserNgEntity
private val roomToken: String?
private val filesArray = mutableListOf<String>()
private val credentials: String
private val baseUrl: String
override fun doWork(): Result {
for (i in filesArray.indices) {
ncApi!!.createRemoteShare(
credentials,
ApiUtils.getSharingUrl(baseUrl),
filesArray[i],
roomToken,
"10"
)
.subscribeOn(Schedulers.io())
.blockingSubscribe(object : Observer<Void?> {
override fun onSubscribe(d: Disposable) {}
override fun onError(e: Throwable) {}
override fun onComplete() {}
override fun onNext(t: Void) {
}
})
}
return Result.success()
}
init {
sharedApplication
?.componentApplication
?.inject(this)
val data = workerParams.inputData
userId = data.getLong(KEY_INTERNAL_USER_ID, 0)
roomToken = data.getString(KEY_ROOM_TOKEN)
Collections.addAll(
filesArray, *data.getStringArray(KEY_FILE_PATHS)
)
operationsUser = usersRepository.getUserWithId(userId)
credentials = operationsUser.getCredentials()
baseUrl = operationsUser.baseUrl
}
}

View File

@ -18,15 +18,17 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.models;
package com.nextcloud.talk.models
import lombok.Data;
import org.parceler.Parcel;
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
import lombok.Data
import org.parceler.Parcel
@Parcelize
data class LoginData (
var serverUrl: String? = null,
var username: String? = null,
var token: String? = null
): Parcelable
@Parcel
@Data
public class LoginData {
String serverUrl;
String username;
String token;
}

View File

@ -65,9 +65,9 @@ public class ChatMessage implements IMessage, MessageContentType, MessageContent
@JsonIgnore
public Long internalUserId = null;
@JsonIgnore
public Long internalMessageId = null;
public String internalMessageId = null;
@JsonIgnore
public Long internalConversationId = null;
public String internalConversationId = null;
@JsonField(name = "id")
@Ignore
public Long jsonMessageId;

View File

@ -30,11 +30,11 @@ import org.parceler.Parcel;
@Parcel
public class ActorWebSocketMessage {
@JsonField(name = "type")
String type;
public String type;
@JsonField(name = "sessionid")
String sessionId;
public String sessionId;
@JsonField(name = "userid")
String userid;
public String userid;
}

View File

@ -30,5 +30,5 @@ import org.parceler.Parcel;
@Parcel
public class BaseWebSocketMessage {
@JsonField(name = "type")
String type;
public String type;
}

View File

@ -31,5 +31,5 @@ import org.parceler.Parcel;
@Data
public class ByeWebSocketMessage extends BaseWebSocketMessage {
@JsonField(name = "bye")
HashMap<String, Object> bye;
public HashMap<String, Object> bye;
}

View File

@ -30,5 +30,5 @@ import org.parceler.Parcel;
@Parcel
public class CallOverallWebSocketMessage extends BaseWebSocketMessage {
@JsonField(name = "message")
CallWebSocketMessage callWebSocketMessage;
public CallWebSocketMessage callWebSocketMessage;
}

View File

@ -31,11 +31,11 @@ import org.parceler.Parcel;
@Parcel
public class CallWebSocketMessage {
@JsonField(name = "recipient")
ActorWebSocketMessage recipientWebSocketMessage;
public ActorWebSocketMessage recipientWebSocketMessage;
@JsonField(name = "sender")
ActorWebSocketMessage senderWebSocketMessage;
public ActorWebSocketMessage senderWebSocketMessage;
@JsonField(name = "data")
NCSignalingMessage ncSignalingMessage;
public NCSignalingMessage ncSignalingMessage;
}

View File

@ -30,5 +30,5 @@ import org.parceler.Parcel;
@JsonObject
public class ErrorOverallWebSocketMessage extends BaseWebSocketMessage {
@JsonField(name = "error")
ErrorWebSocketMessage errorWebSocketMessage;
public ErrorWebSocketMessage errorWebSocketMessage;
}

View File

@ -30,8 +30,8 @@ import org.parceler.Parcel;
@JsonObject
public class ErrorWebSocketMessage {
@JsonField(name = "code")
String code;
public String code;
@JsonField(name = "message")
String message;
public String message;
}

View File

@ -31,7 +31,7 @@ import org.parceler.Parcel;
@JsonObject
public class EventOverallWebSocketMessage extends BaseWebSocketMessage {
@JsonField(name = "type")
String type;
public String type;
@JsonField(name = "event")
HashMap<String, Object> eventMap;
public HashMap<String, Object> eventMap;
}

View File

@ -30,5 +30,5 @@ import org.parceler.Parcel;
@Parcel
public class HelloResponseOverallWebSocketMessage extends BaseWebSocketMessage {
@JsonField(name = "hello")
HelloResponseWebSocketMessage helloResponseWebSocketMessage;
public HelloResponseWebSocketMessage helloResponseWebSocketMessage;
}

View File

@ -30,13 +30,13 @@ import org.parceler.Parcel;
@Parcel
public class HelloResponseWebSocketMessage {
@JsonField(name = "resumeid")
String resumeId;
public String resumeId;
@JsonField(name = "sessionid")
String sessionId;
public String sessionId;
@JsonField(name = "server")
ServerHelloResponseFeaturesWebSocketMessage serverHelloResponseFeaturesWebSocketMessage;
public ServerHelloResponseFeaturesWebSocketMessage serverHelloResponseFeaturesWebSocketMessage;
public boolean serverHasMCUSupport() {
return serverHelloResponseFeaturesWebSocketMessage != null

View File

@ -30,5 +30,5 @@ import org.parceler.Parcel;
@Parcel
public class JoinedRoomOverallWebSocketMessage extends BaseWebSocketMessage {
@JsonField(name = "room")
RoomWebSocketMessage roomWebSocketMessage;
public RoomWebSocketMessage roomWebSocketMessage;
}

View File

@ -32,8 +32,8 @@ import org.parceler.Parcel;
@JsonObject
public class RoomPropertiesWebSocketMessage {
@JsonField(name = "name")
String name;
public String name;
@JsonField(name = "type", typeConverter = EnumRoomTypeConverter.class)
Conversation.ConversationType roomType;
public Conversation.ConversationType roomType;
}

View File

@ -30,11 +30,11 @@ import org.parceler.Parcel;
@Parcel
public class RoomWebSocketMessage {
@JsonField(name = "roomid")
String roomId;
public String roomId;
@JsonField(name = "sessionid")
String sessiondId;
public String sessiondId;
@JsonField(name = "properties")
RoomPropertiesWebSocketMessage roomPropertiesWebSocketMessage;
public RoomPropertiesWebSocketMessage roomPropertiesWebSocketMessage;
}

View File

@ -31,5 +31,5 @@ import org.parceler.Parcel;
@Data
public class ServerHelloResponseFeaturesWebSocketMessage {
@JsonField(name = "features")
List<String> features;
public List<String> features;
}

View File

@ -37,4 +37,20 @@ class UsersRepositoryImpl(val usersDao: UsersDao): UsersRepository {
override fun getUsers(): List<UserNgEntity> {
return usersDao.getUsers()
}
override fun getUserWithId(id: Long): UserNgEntity {
return usersDao.getUserWithId(id)
}
override suspend fun getUserWithUsernameAndServer(
username: String,
server: String
): UserNgEntity? {
return usersDao.getUserWithUsernameAndServer(username, server)
}
override suspend fun updateUser(user: UserNgEntity): Int {
return usersDao.updateUser(user)
}
}

View File

@ -25,6 +25,9 @@ import com.nextcloud.talk.newarch.local.models.UserNgEntity
interface UsersRepository {
fun getActiveUserLiveData(): LiveData<UserNgEntity>
fun getActiveUser(): UserNgEntity
fun getActiveUser(): UserNgEntity?
fun getUsers(): List<UserNgEntity>
fun getUserWithId(id: Long): UserNgEntity
suspend fun getUserWithUsernameAndServer(username: String, server: String): UserNgEntity?
suspend fun updateUser(user: UserNgEntity): Int
}

View File

@ -27,6 +27,7 @@ import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update
import com.nextcloud.talk.newarch.local.models.ConversationEntity
@Dao
@ -39,10 +40,10 @@ abstract class ConversationsDao {
abstract suspend fun clearConversationsForUser(userId: Long)
@Insert(onConflict = OnConflictStrategy.REPLACE)
abstract suspend fun saveConversation(conversation: ConversationEntity)
abstract suspend fun saveConversationWithInsert(conversation: ConversationEntity): Long
@Insert(onConflict = OnConflictStrategy.REPLACE)
abstract suspend fun saveConversations(vararg conversations: ConversationEntity)
abstract suspend fun saveConversationsWithInsert(vararg conversations: ConversationEntity): List<Long>
@Query(
"UPDATE conversations SET changing = :changing WHERE user = :userId AND conversation_id = :conversationId"
@ -80,17 +81,16 @@ abstract class ConversationsDao {
@Transaction
open suspend fun updateConversationsForUser(
userId: Long,
newConversations:
Array<ConversationEntity>
newConversations: Array<ConversationEntity>
) {
val timestamp = System.currentTimeMillis()
val conversationsWithTimestampApplied = newConversations.map {
it.modifiedAt = System.currentTimeMillis()
it.modifiedAt = timestamp
it
}
saveConversations(*conversationsWithTimestampApplied.toTypedArray())
saveConversationsWithInsert(*conversationsWithTimestampApplied.toTypedArray())
deleteConversationsForUserWithTimestamp(userId, timestamp)
}
}

View File

@ -25,6 +25,9 @@ import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update
import com.nextcloud.talk.newarch.local.models.ConversationEntity
import com.nextcloud.talk.newarch.local.models.MessageEntity
@Dao
@ -34,5 +37,5 @@ abstract class MessagesDao {
LiveData<List<MessageEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
abstract suspend fun saveMessages(vararg messages: MessageEntity)
abstract suspend fun saveMessagesWithInsert(vararg messages: MessageEntity): List<Long>
}

View File

@ -25,6 +25,7 @@ import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import com.nextcloud.talk.models.database.UserEntity
import com.nextcloud.talk.newarch.local.models.ConversationEntity
import com.nextcloud.talk.newarch.local.models.UserNgEntity
@ -41,6 +42,9 @@ abstract class UsersDao {
@Query("DELETE FROM users WHERE id = :userId")
abstract fun deleteUserForId(userId: Long)
@Update
abstract suspend fun updateUser(user: UserNgEntity): Int
@Insert(onConflict = OnConflictStrategy.REPLACE)
abstract fun saveUser(user: UserNgEntity)
@ -51,8 +55,13 @@ abstract class UsersDao {
@Query("SELECT * FROM users where status != 2")
abstract fun getUsers(): List<UserNgEntity>
@Query("SELECT * FROM users where id = :id")
abstract fun getUserWithId(id: Long): UserNgEntity
@Query("SELECT * FROM users where status = 2")
abstract fun getUsersScheduledForDeletion(): List<UserNgEntity>
@Query("SELECT * FROM users WHERE username = :username AND base_url = :server")
abstract suspend fun getUserWithUsernameAndServer(username: String, server: String): UserNgEntity?
}

View File

@ -48,7 +48,7 @@ import java.util.HashMap
)]
)
data class ConversationEntity(
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") var id: Long? = null,
@PrimaryKey @ColumnInfo(name = "id") var id: String,
@ColumnInfo(name = "user") var user: Long? = null,
@ColumnInfo(name = "conversation_id") var conversationId: String? = null,
@ColumnInfo(name = "token") var token: String? = null,
@ -117,7 +117,7 @@ fun ConversationEntity.toConversation(): Conversation {
}
fun Conversation.toConversationEntity(): ConversationEntity {
val conversationEntity = ConversationEntity()
val conversationEntity = ConversationEntity(this.internalUserId.toString() + "@" + this.conversationId)
conversationEntity.user = this.internalUserId
conversationEntity.conversationId = this.conversationId
conversationEntity.token = this.token

View File

@ -46,8 +46,8 @@ import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType
)]
)
data class MessageEntity(
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") var id: Long? = null,
@ColumnInfo(name = "conversation") var conversation: Long? = null,
@PrimaryKey @ColumnInfo(name = "id") var id: String,
@ColumnInfo(name = "conversation") var conversation: String,
@ColumnInfo(name = "message_id") var messageId: Long = 0,
@ColumnInfo(name = "actor_id") var actorId: String? = null,
@ColumnInfo(name = "actor_type") var actorType: String? = null,
@ -77,8 +77,7 @@ fun MessageEntity.toChatMessage(): ChatMessage {
@SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
fun ChatMessage.toMessageEntity(): MessageEntity {
val messageEntity = MessageEntity(this.internalMessageId)
messageEntity.conversation = this.internalConversationId
val messageEntity = MessageEntity(this.internalConversationId + "@" + this.jsonMessageId, this.activeUser.id.toString() + "@" + this.internalConversationId)
messageEntity.messageId = this.jsonMessageId
messageEntity.actorType = this.actorType
messageEntity.actorId = this.actorId

View File

@ -1,473 +0,0 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.webrtc;
import android.content.Context;
import android.text.TextUtils;
import android.util.Log;
import autodagger.AutoInjector;
import com.bluelinelabs.logansquare.LoganSquare;
import com.nextcloud.talk.R;
import com.nextcloud.talk.application.NextcloudTalkApplication;
import com.nextcloud.talk.events.NetworkEvent;
import com.nextcloud.talk.events.WebSocketCommunicationEvent;
import com.nextcloud.talk.models.database.UserEntity;
import com.nextcloud.talk.models.json.participants.Participant;
import com.nextcloud.talk.models.json.signaling.NCMessageWrapper;
import com.nextcloud.talk.models.json.signaling.NCSignalingMessage;
import com.nextcloud.talk.models.json.websocket.BaseWebSocketMessage;
import com.nextcloud.talk.models.json.websocket.ByeWebSocketMessage;
import com.nextcloud.talk.models.json.websocket.CallOverallWebSocketMessage;
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;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
import okio.ByteString;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
@AutoInjector(NextcloudTalkApplication.class)
public class MagicWebSocketInstance extends WebSocketListener {
private static final String TAG = "MagicWebSocketInstance";
@Inject
OkHttpClient okHttpClient;
@Inject
EventBus eventBus;
@Inject
Context context;
private UserNgEntity conversationUser;
private String webSocketTicket;
private String resumeId;
private String sessionId;
private boolean hasMCU;
private boolean connected;
private WebSocketConnectionHelper webSocketConnectionHelper;
private WebSocket internalWebSocket;
private MagicMap magicMap;
private String connectionUrl;
private String currentRoomToken;
private int restartCount = 0;
private boolean reconnecting = false;
private HashMap<String, Participant> usersHashMap;
private List<String> messagesQueue = new ArrayList<>();
MagicWebSocketInstance(UserNgEntity conversationUser, String connectionUrl,
String webSocketTicket) {
NextcloudTalkApplication.Companion.getSharedApplication()
.getComponentApplication()
.inject(this);
this.connectionUrl = connectionUrl;
this.conversationUser = conversationUser;
this.webSocketTicket = webSocketTicket;
this.webSocketConnectionHelper = new WebSocketConnectionHelper();
this.usersHashMap = new HashMap<>();
magicMap = new MagicMap();
connected = false;
eventBus.register(this);
restartWebSocket();
}
private void sendHello() {
try {
if (TextUtils.isEmpty(resumeId)) {
internalWebSocket.send(LoganSquare.serialize(
webSocketConnectionHelper.getAssembledHelloModel(conversationUser, webSocketTicket)));
} else {
internalWebSocket.send(LoganSquare.serialize(
webSocketConnectionHelper.getAssembledHelloModelForResume(resumeId)));
}
} catch (IOException e) {
Log.e(TAG, "Failed to serialize hello model");
}
}
@Override
public void onOpen(WebSocket webSocket, Response response) {
internalWebSocket = webSocket;
sendHello();
}
private void closeWebSocket(WebSocket webSocket) {
webSocket.close(1000, null);
webSocket.cancel();
if (webSocket == internalWebSocket) {
connected = false;
messagesQueue = new ArrayList<>();
}
restartWebSocket();
}
public void clearResumeId() {
resumeId = "";
}
public void restartWebSocket() {
reconnecting = true;
Request request = new Request.Builder().url(connectionUrl).build();
okHttpClient.newWebSocket(request, this);
restartCount++;
}
@Override
public void onMessage(WebSocket webSocket, String text) {
if (webSocket == internalWebSocket) {
Log.d(TAG, "Receiving : " + webSocket.toString() + " " + text);
LoggingUtils.INSTANCE.writeLogEntryToFile(context,
"WebSocket " + webSocket.hashCode() + " receiving: " + text);
try {
BaseWebSocketMessage baseWebSocketMessage =
LoganSquare.parse(text, BaseWebSocketMessage.class);
String messageType = baseWebSocketMessage.getType();
switch (messageType) {
case "hello":
connected = true;
reconnecting = false;
restartCount = 0;
String oldResumeId = resumeId;
HelloResponseOverallWebSocketMessage helloResponseWebSocketMessage =
LoganSquare.parse(text, HelloResponseOverallWebSocketMessage.class);
resumeId =
helloResponseWebSocketMessage.getHelloResponseWebSocketMessage().getResumeId();
sessionId =
helloResponseWebSocketMessage.getHelloResponseWebSocketMessage().getSessionId();
hasMCU = helloResponseWebSocketMessage.getHelloResponseWebSocketMessage()
.serverHasMCUSupport();
for (int i = 0; i < messagesQueue.size(); i++) {
webSocket.send(messagesQueue.get(i));
}
messagesQueue = new ArrayList<>();
HashMap<String, String> helloHasHap = new HashMap<>();
if (!TextUtils.isEmpty(oldResumeId)) {
helloHasHap.put("oldResumeId", oldResumeId);
} else {
currentRoomToken = "";
}
if (!TextUtils.isEmpty(currentRoomToken)) {
helloHasHap.put("roomToken", currentRoomToken);
}
eventBus.post(new WebSocketCommunicationEvent("hello", helloHasHap));
break;
case "error":
ErrorOverallWebSocketMessage errorOverallWebSocketMessage =
LoganSquare.parse(text, ErrorOverallWebSocketMessage.class);
if (("no_such_session").equals(
errorOverallWebSocketMessage.getErrorWebSocketMessage().getCode())) {
LoggingUtils.INSTANCE.writeLogEntryToFile(context,
"WebSocket " + webSocket.hashCode() + " resumeID " + resumeId + " expired");
resumeId = "";
currentRoomToken = "";
restartWebSocket();
} else if (("hello_expected").equals(
errorOverallWebSocketMessage.getErrorWebSocketMessage().getCode())) {
restartWebSocket();
}
break;
case "room":
JoinedRoomOverallWebSocketMessage joinedRoomOverallWebSocketMessage =
LoganSquare.parse(text, JoinedRoomOverallWebSocketMessage.class);
currentRoomToken =
joinedRoomOverallWebSocketMessage.getRoomWebSocketMessage().getRoomId();
if (joinedRoomOverallWebSocketMessage.getRoomWebSocketMessage()
.getRoomPropertiesWebSocketMessage() != null && !TextUtils.isEmpty(
currentRoomToken)) {
sendRoomJoinedEvent();
}
break;
case "event":
EventOverallWebSocketMessage eventOverallWebSocketMessage =
LoganSquare.parse(text, EventOverallWebSocketMessage.class);
if (eventOverallWebSocketMessage.getEventMap() != null) {
String target = (String) eventOverallWebSocketMessage.getEventMap().get("target");
switch (target) {
case "room":
if (eventOverallWebSocketMessage.getEventMap().get("type").equals("message")) {
Map<String, Object> messageHashMap =
(Map<String, Object>) eventOverallWebSocketMessage.getEventMap()
.get("message");
if (messageHashMap.containsKey("data")) {
Map<String, Object> dataHashMap = (Map<String, Object>) messageHashMap.get(
"data");
if (dataHashMap.containsKey("chat")) {
boolean shouldRefreshChat;
Map<String, Object> chatMap = (Map<String, Object>) dataHashMap.get("chat");
if (chatMap.containsKey("refresh")) {
shouldRefreshChat = (boolean) chatMap.get("refresh");
if (shouldRefreshChat) {
HashMap<String, String> refreshChatHashMap = new HashMap<>();
refreshChatHashMap.put(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN(),
(String) messageHashMap.get("roomid"));
refreshChatHashMap.put(BundleKeys.INSTANCE.getKEY_INTERNAL_USER_ID(),
Long.toString(conversationUser.getId()));
eventBus.post(
new WebSocketCommunicationEvent("refreshChat", refreshChatHashMap));
}
}
}
}
} else if (eventOverallWebSocketMessage.getEventMap()
.get("type")
.equals("join")) {
List<HashMap<String, Object>> joinEventMap =
(List<HashMap<String, Object>>) eventOverallWebSocketMessage.getEventMap()
.get("join");
HashMap<String, Object> internalHashMap;
Participant participant;
for (int i = 0; i < joinEventMap.size(); i++) {
internalHashMap = joinEventMap.get(i);
HashMap<String, Object> userMap =
(HashMap<String, Object>) internalHashMap.get("user");
participant = new Participant();
participant.setUserId((String) internalHashMap.get("userid"));
participant.setDisplayName((String) userMap.get("displayname"));
usersHashMap.put((String) internalHashMap.get("sessionid"), participant);
}
}
break;
case "participants":
if (eventOverallWebSocketMessage.getEventMap().get("type").equals("update")) {
HashMap<String, String> refreshChatHashMap = new HashMap<>();
HashMap<String, Object> updateEventMap =
(HashMap<String, Object>) eventOverallWebSocketMessage.getEventMap()
.get("update");
refreshChatHashMap.put("roomToken", (String) updateEventMap.get("roomid"));
refreshChatHashMap.put("jobId",
Integer.toString(magicMap.add(updateEventMap.get("users"))));
eventBus.post(
new WebSocketCommunicationEvent("participantsUpdate", refreshChatHashMap));
}
break;
}
}
break;
case "message":
CallOverallWebSocketMessage callOverallWebSocketMessage =
LoganSquare.parse(text, CallOverallWebSocketMessage.class);
NCSignalingMessage ncSignalingMessage =
callOverallWebSocketMessage.getCallWebSocketMessage().getNcSignalingMessage();
if (TextUtils.isEmpty(ncSignalingMessage.getFrom())
&& callOverallWebSocketMessage.getCallWebSocketMessage().getSenderWebSocketMessage()
!= null) {
ncSignalingMessage.setFrom(callOverallWebSocketMessage.getCallWebSocketMessage()
.getSenderWebSocketMessage()
.getSessionId());
}
if (!TextUtils.isEmpty(ncSignalingMessage.getFrom())) {
HashMap<String, String> messageHashMap = new HashMap<>();
messageHashMap.put("jobId", Integer.toString(magicMap.add(ncSignalingMessage)));
eventBus.post(new WebSocketCommunicationEvent("signalingMessage", messageHashMap));
}
break;
case "bye":
connected = false;
resumeId = "";
default:
break;
}
} catch (IOException e) {
LoggingUtils.INSTANCE.writeLogEntryToFile(context,
"WebSocket " + webSocket.hashCode() + " IOException: " + e.getMessage());
Log.e(TAG, "Failed to recognize WebSocket message");
}
}
}
private void sendRoomJoinedEvent() {
HashMap<String, String> joinRoomHashMap = new HashMap<>();
joinRoomHashMap.put("roomToken", currentRoomToken);
eventBus.post(new WebSocketCommunicationEvent("roomJoined", joinRoomHashMap));
}
@Override
public void onMessage(WebSocket webSocket, ByteString bytes) {
Log.d(TAG, "Receiving bytes : " + bytes.hex());
}
@Override
public void onClosing(WebSocket webSocket, int code, String reason) {
Log.d(TAG, "Closing : " + code + " / " + reason);
LoggingUtils.INSTANCE.writeLogEntryToFile(context,
"WebSocket " + webSocket.hashCode() + " Closing: " + reason);
}
@Override
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
Log.d(TAG, "Error : " + t.getMessage());
LoggingUtils.INSTANCE.writeLogEntryToFile(context,
"WebSocket " + webSocket.hashCode() + " onFailure: " + t.getMessage());
closeWebSocket(webSocket);
}
public String getSessionId() {
return sessionId;
}
public boolean hasMCU() {
return hasMCU;
}
public void joinRoomWithRoomTokenAndSession(String roomToken, String normalBackendSession) {
try {
String message = LoganSquare.serialize(
webSocketConnectionHelper.getAssembledJoinOrLeaveRoomModel(roomToken,
normalBackendSession));
if (!connected || reconnecting) {
messagesQueue.add(message);
} else {
if (roomToken.equals(currentRoomToken)) {
sendRoomJoinedEvent();
} else {
internalWebSocket.send(message);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public void sendCallMessage(NCMessageWrapper ncMessageWrapper) {
try {
String message = LoganSquare.serialize(
webSocketConnectionHelper.getAssembledCallMessageModel(ncMessageWrapper));
if (!connected || reconnecting) {
messagesQueue.add(message);
} else {
internalWebSocket.send(message);
}
} catch (IOException e) {
LoggingUtils.INSTANCE.writeLogEntryToFile(context,
"WebSocket sendCalLMessage: " + e.getMessage() + "\n" + ncMessageWrapper.toString());
Log.e(TAG, "Failed to serialize signaling message");
}
}
public Object getJobWithId(Integer id) {
Object copyJob = magicMap.get(id);
magicMap.remove(id);
return copyJob;
}
public void requestOfferForSessionIdWithType(String sessionIdParam, String roomType) {
try {
String message = LoganSquare.serialize(
webSocketConnectionHelper.getAssembledRequestOfferModel(sessionIdParam, roomType));
if (!connected || reconnecting) {
messagesQueue.add(message);
} else {
internalWebSocket.send(message);
}
} catch (IOException e) {
LoggingUtils.INSTANCE.writeLogEntryToFile(context,
"WebSocket requestOfferForSessionIdWithType: "
+ e.getMessage()
+ "\n"
+ sessionIdParam
+ " "
+ roomType);
Log.e(TAG, "Failed to offer request");
}
}
void sendBye() {
if (connected) {
try {
ByeWebSocketMessage byeWebSocketMessage = new ByeWebSocketMessage();
byeWebSocketMessage.setType("bye");
byeWebSocketMessage.setBye(new HashMap<>());
internalWebSocket.send(LoganSquare.serialize(byeWebSocketMessage));
} catch (IOException e) {
Log.e(TAG, "Failed to serialize bye message");
}
}
}
public boolean isConnected() {
return connected;
}
public String getDisplayNameForSession(String session) {
if (usersHashMap.containsKey(session)) {
return usersHashMap.get(session).getDisplayName();
}
return NextcloudTalkApplication.Companion.getSharedApplication()
.getString(R.string.nc_nick_guest);
}
public String getSessionForUserId(String userId) {
for (String session : usersHashMap.keySet()) {
if (userId.equals(usersHashMap.get(session).getUserId())) {
return session;
}
}
return "";
}
public String getUserIdForSession(String session) {
if (usersHashMap.containsKey(session)) {
return usersHashMap.get(session).getUserId();
}
return "";
}
@Subscribe(threadMode = ThreadMode.BACKGROUND)
public void onMessageEvent(NetworkEvent networkEvent) {
if (networkEvent.getNetworkConnectionEvent()
.equals(NetworkEvent.NetworkConnectionEvent.NETWORK_CONNECTED) && !isConnected()) {
restartWebSocket();
}
}
}

View File

@ -0,0 +1,530 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.webrtc
import android.content.Context
import android.text.TextUtils
import android.util.Log
import autodagger.AutoInjector
import com.bluelinelabs.logansquare.LoganSquare
import com.nextcloud.talk.R.string
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.events.NetworkEvent
import com.nextcloud.talk.events.NetworkEvent.NetworkConnectionEvent.NETWORK_CONNECTED
import com.nextcloud.talk.events.WebSocketCommunicationEvent
import com.nextcloud.talk.models.json.participants.Participant
import com.nextcloud.talk.models.json.signaling.NCMessageWrapper
import com.nextcloud.talk.models.json.websocket.BaseWebSocketMessage
import com.nextcloud.talk.models.json.websocket.ByeWebSocketMessage
import com.nextcloud.talk.models.json.websocket.CallOverallWebSocketMessage
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.writeLogEntryToFile
import com.nextcloud.talk.utils.MagicMap
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
import okhttp3.OkHttpClient
import okhttp3.Request.Builder
import okhttp3.Response
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import okio.ByteString
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode.BACKGROUND
import org.koin.core.KoinComponent
import org.koin.core.inject
import java.io.IOException
import java.util.ArrayList
import java.util.HashMap
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
class MagicWebSocketInstance internal constructor(
conversationUser: UserNgEntity,
connectionUrl: String,
webSocketTicket: String
) : WebSocketListener(), KoinComponent {
val okHttpClient: OkHttpClient by inject()
val eventBus: EventBus by inject()
val context: Context by inject()
private val conversationUser: UserNgEntity
private val webSocketTicket: String
private var resumeId: String? = null
var sessionId: String? = null
private set
private var hasMCU = false
var isConnected: Boolean
private set
private val webSocketConnectionHelper: WebSocketConnectionHelper
private var internalWebSocket: WebSocket? = null
private val magicMap: MagicMap
private val connectionUrl: String
private var currentRoomToken: String? = null
private var restartCount = 0
private var reconnecting = false
private val usersHashMap: HashMap<String?, Participant>
private var messagesQueue: MutableList<String> =
ArrayList()
private fun sendHello() {
try {
if (TextUtils.isEmpty(resumeId)) {
internalWebSocket!!.send(
LoganSquare.serialize(
webSocketConnectionHelper.getAssembledHelloModel(conversationUser, webSocketTicket)
)
)
} else {
internalWebSocket!!.send(
LoganSquare.serialize(
webSocketConnectionHelper.getAssembledHelloModelForResume(resumeId)
)
)
}
} catch (e: IOException) {
Log.e(TAG, "Failed to serialize hello model")
}
}
override fun onOpen(
webSocket: WebSocket,
response: Response
) {
internalWebSocket = webSocket
sendHello()
}
private fun closeWebSocket(webSocket: WebSocket) {
webSocket.close(1000, null)
webSocket.cancel()
if (webSocket === internalWebSocket) {
isConnected = false
messagesQueue = ArrayList()
}
restartWebSocket()
}
fun clearResumeId() {
resumeId = ""
}
fun restartWebSocket() {
reconnecting = true
val request = Builder()
.url(connectionUrl)
.build()
okHttpClient.newWebSocket(request, this)
restartCount++
}
override fun onMessage(
webSocket: WebSocket,
text: String
) {
if (webSocket === internalWebSocket) {
Log.d(
TAG, "Receiving : $webSocket $text"
)
writeLogEntryToFile(
context,
"WebSocket " + webSocket.hashCode() + " receiving: " + text
)
try {
val baseWebSocketMessage =
LoganSquare.parse(text, BaseWebSocketMessage::class.java)
val messageType = baseWebSocketMessage.type
when (messageType) {
"hello" -> {
isConnected = true
reconnecting = false
restartCount = 0
val oldResumeId = resumeId
val helloResponseWebSocketMessage =
LoganSquare.parse(
text, HelloResponseOverallWebSocketMessage::class.java
)
resumeId = helloResponseWebSocketMessage.helloResponseWebSocketMessage
.resumeId
sessionId = helloResponseWebSocketMessage.helloResponseWebSocketMessage
.sessionId
hasMCU = helloResponseWebSocketMessage.helloResponseWebSocketMessage
.serverHasMCUSupport()
var i = 0
while (i < messagesQueue.size) {
webSocket.send(messagesQueue[i])
i++
}
messagesQueue = ArrayList()
val helloHasHap =
HashMap<String, String?>()
if (!TextUtils.isEmpty(oldResumeId)) {
helloHasHap["oldResumeId"] = oldResumeId
} else {
currentRoomToken = ""
}
if (!TextUtils.isEmpty(currentRoomToken)) {
helloHasHap["roomToken"] = currentRoomToken
}
eventBus.post(WebSocketCommunicationEvent("hello", helloHasHap))
}
"error" -> {
val errorOverallWebSocketMessage =
LoganSquare.parse(
text, ErrorOverallWebSocketMessage::class.java
)
if ("no_such_session" ==
errorOverallWebSocketMessage.errorWebSocketMessage.code
) {
writeLogEntryToFile(
context,
"WebSocket " + webSocket.hashCode() + " resumeID " + resumeId + " expired"
)
resumeId = ""
currentRoomToken = ""
restartWebSocket()
} else if ("hello_expected" ==
errorOverallWebSocketMessage.errorWebSocketMessage.code
) {
restartWebSocket()
}
}
"room" -> {
val joinedRoomOverallWebSocketMessage =
LoganSquare.parse(
text, JoinedRoomOverallWebSocketMessage::class.java
)
currentRoomToken = joinedRoomOverallWebSocketMessage.roomWebSocketMessage
.roomId
if (joinedRoomOverallWebSocketMessage.roomWebSocketMessage
.roomPropertiesWebSocketMessage != null && !TextUtils.isEmpty(
currentRoomToken
)
) {
sendRoomJoinedEvent()
}
}
"event" -> {
val eventOverallWebSocketMessage =
LoganSquare.parse(
text, EventOverallWebSocketMessage::class.java
)
if (eventOverallWebSocketMessage.eventMap != null) {
val target =
eventOverallWebSocketMessage.eventMap["target"] as String?
when (target) {
"room" -> if (eventOverallWebSocketMessage.eventMap["type"] == "message"
) {
val messageHashMap =
eventOverallWebSocketMessage.eventMap["message"] as Map<String, Any>?
if (messageHashMap!!.containsKey("data")) {
val dataHashMap =
messageHashMap["data"] as Map<String, Any>?
if (dataHashMap!!.containsKey("chat")) {
val shouldRefreshChat: Boolean
val chatMap =
dataHashMap["chat"] as Map<String, Any>?
if (chatMap!!.containsKey("refresh")) {
shouldRefreshChat = chatMap["refresh"] as Boolean
if (shouldRefreshChat) {
val refreshChatHashMap =
HashMap<String, String?>()
refreshChatHashMap[KEY_ROOM_TOKEN] = messageHashMap["roomid"] as String?
refreshChatHashMap[KEY_INTERNAL_USER_ID] =
java.lang.Long.toString(conversationUser.id)
eventBus.post(
WebSocketCommunicationEvent("refreshChat", refreshChatHashMap)
)
}
}
}
}
} else if (eventOverallWebSocketMessage.eventMap["type"]
== "join"
) {
val joinEventMap =
eventOverallWebSocketMessage.eventMap["join"] as List<HashMap<String, Any>>?
var internalHashMap: HashMap<String, Any>
var participant: Participant
var i = 0
while (i < joinEventMap!!.size) {
internalHashMap = joinEventMap[i]
val userMap =
internalHashMap["user"] as HashMap<String, Any>?
participant = Participant()
participant.userId = internalHashMap["userid"] as String
participant.displayName = userMap!!["displayname"] as String
usersHashMap[internalHashMap["sessionid"] as String?] = participant
i++
}
}
"participants" -> if (eventOverallWebSocketMessage.eventMap["type"] == "update"
) {
val refreshChatHashMap =
HashMap<String, String?>()
val updateEventMap =
eventOverallWebSocketMessage.eventMap["update"] as HashMap<String, Any>?
refreshChatHashMap["roomToken"] = updateEventMap!!["roomid"] as String?
refreshChatHashMap["jobId"] = Integer.toString(
magicMap.add(
updateEventMap["users"]!!
)
)
eventBus.post(
WebSocketCommunicationEvent("participantsUpdate", refreshChatHashMap)
)
}
}
}
}
"message" -> {
val callOverallWebSocketMessage =
LoganSquare.parse(
text, CallOverallWebSocketMessage::class.java
)
val ncSignalingMessage =
callOverallWebSocketMessage.callWebSocketMessage
.ncSignalingMessage
if (TextUtils.isEmpty(ncSignalingMessage.from)
&& callOverallWebSocketMessage.callWebSocketMessage.senderWebSocketMessage
!= null
) {
ncSignalingMessage.from =
callOverallWebSocketMessage.callWebSocketMessage
.senderWebSocketMessage
.sessionId
}
if (!TextUtils.isEmpty(ncSignalingMessage.from)) {
val messageHashMap =
HashMap<String, String>()
messageHashMap["jobId"] = Integer.toString(magicMap.add(ncSignalingMessage))
eventBus.post(WebSocketCommunicationEvent("signalingMessage", messageHashMap))
}
}
"bye" -> {
isConnected = false
resumeId = ""
}
else -> {
}
}
} catch (e: IOException) {
writeLogEntryToFile(
context,
"WebSocket " + webSocket.hashCode() + " IOException: " + e.message
)
Log.e(
TAG, "Failed to recognize WebSocket message"
)
}
}
}
private fun sendRoomJoinedEvent() {
val joinRoomHashMap =
HashMap<String, String?>()
joinRoomHashMap["roomToken"] = currentRoomToken
eventBus.post(WebSocketCommunicationEvent("roomJoined", joinRoomHashMap))
}
override fun onMessage(
webSocket: WebSocket,
bytes: ByteString
) {
Log.d(TAG, "Receiving bytes : " + bytes.hex())
}
override fun onClosing(
webSocket: WebSocket,
code: Int,
reason: String
) {
Log.d(TAG, "Closing : $code / $reason")
writeLogEntryToFile(
context,
"WebSocket " + webSocket.hashCode() + " Closing: " + reason
)
}
override fun onFailure(
webSocket: WebSocket,
t: Throwable,
response: Response?
) {
Log.d(TAG, "Error : " + t.message)
writeLogEntryToFile(
context,
"WebSocket " + webSocket.hashCode() + " onFailure: " + t.message
)
closeWebSocket(webSocket)
}
fun hasMCU(): Boolean {
return hasMCU
}
fun joinRoomWithRoomTokenAndSession(
roomToken: String,
normalBackendSession: String?
) {
try {
val message = LoganSquare.serialize(
webSocketConnectionHelper.getAssembledJoinOrLeaveRoomModel(
roomToken,
normalBackendSession
)
)
if (!isConnected || reconnecting) {
messagesQueue.add(message)
} else {
if (roomToken == currentRoomToken) {
sendRoomJoinedEvent()
} else {
internalWebSocket!!.send(message)
}
}
} catch (e: IOException) {
e.printStackTrace()
}
}
fun sendCallMessage(ncMessageWrapper: NCMessageWrapper) {
try {
val message = LoganSquare.serialize(
webSocketConnectionHelper.getAssembledCallMessageModel(ncMessageWrapper)
)
if (!isConnected || reconnecting) {
messagesQueue.add(message)
} else {
internalWebSocket!!.send(message)
}
} catch (e: IOException) {
writeLogEntryToFile(
context,
"WebSocket sendCalLMessage: " + e.message + "\n" + ncMessageWrapper.toString()
)
Log.e(
TAG, "Failed to serialize signaling message"
)
}
}
fun getJobWithId(id: Int): Any? {
val copyJob = magicMap[id]
magicMap.remove(id)
return copyJob
}
fun requestOfferForSessionIdWithType(
sessionIdParam: String,
roomType: String
) {
try {
val message = LoganSquare.serialize(
webSocketConnectionHelper.getAssembledRequestOfferModel(sessionIdParam, roomType)
)
if (!isConnected || reconnecting) {
messagesQueue.add(message)
} else {
internalWebSocket!!.send(message)
}
} catch (e: IOException) {
writeLogEntryToFile(
context,
"WebSocket requestOfferForSessionIdWithType: "
+ e.message
+ "\n"
+ sessionIdParam
+ " "
+ roomType
)
Log.e(TAG, "Failed to offer request")
}
}
fun sendBye() {
if (isConnected) {
try {
val byeWebSocketMessage = ByeWebSocketMessage()
byeWebSocketMessage.type = "bye"
byeWebSocketMessage.bye = HashMap()
internalWebSocket!!.send(LoganSquare.serialize(byeWebSocketMessage))
} catch (e: IOException) {
Log.e(TAG, "Failed to serialize bye message")
}
}
}
fun getDisplayNameForSession(session: String?): String {
return if (usersHashMap.containsKey(session)) {
usersHashMap[session]!!.displayName
} else sharedApplication!!.getString(string.nc_nick_guest)
}
fun getSessionForUserId(userId: String): String? {
for (session in usersHashMap.keys) {
if (userId == usersHashMap[session]!!.userId) {
return session
}
}
return ""
}
fun getUserIdForSession(session: String?): String {
return if (usersHashMap.containsKey(session)) {
usersHashMap[session]!!.userId
} else ""
}
@Subscribe(threadMode = BACKGROUND)
fun onMessageEvent(networkEvent: NetworkEvent) {
if ((networkEvent.networkConnectionEvent
== NETWORK_CONNECTED) && !isConnected
) {
restartWebSocket()
}
}
companion object {
private const val TAG = "MagicWebSocketInstance"
}
init {
sharedApplication
?.componentApplication
?.inject(this)
this.connectionUrl = connectionUrl
this.conversationUser = conversationUser
this.webSocketTicket = webSocketTicket
webSocketConnectionHelper = WebSocketConnectionHelper()
usersHashMap =
HashMap()
magicMap = MagicMap()
isConnected = false
eventBus.register(this)
restartWebSocket()
}
}

View File

@ -29,7 +29,7 @@
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-XX:MaxHeapSize\=4096m -Xmx4096m
org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m
android.useAndroidX=true
android.enableJetifier=true