mirror of
https://github.com/nextcloud/talk-android
synced 2025-06-21 20:49:36 +01:00
Significant changes
Signed-off-by: Mario Danic <mario@lovelyhq.com>
This commit is contained in:
parent
d5cf261776
commit
8eb2f78a23
@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
@ -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,11 +1320,13 @@ 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"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OnClick(R.id.pip_video_view, R.id.remote_renderers_layout)
|
||||
fun showCallControls() {
|
||||
@ -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"
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -30,5 +30,5 @@ import org.parceler.Parcel;
|
||||
@Parcel
|
||||
public class BaseWebSocketMessage {
|
||||
@JsonField(name = "type")
|
||||
String type;
|
||||
public String type;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -30,5 +30,5 @@ import org.parceler.Parcel;
|
||||
@Parcel
|
||||
public class CallOverallWebSocketMessage extends BaseWebSocketMessage {
|
||||
@JsonField(name = "message")
|
||||
CallWebSocketMessage callWebSocketMessage;
|
||||
public CallWebSocketMessage callWebSocketMessage;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -30,5 +30,5 @@ import org.parceler.Parcel;
|
||||
@JsonObject
|
||||
public class ErrorOverallWebSocketMessage extends BaseWebSocketMessage {
|
||||
@JsonField(name = "error")
|
||||
ErrorWebSocketMessage errorWebSocketMessage;
|
||||
public ErrorWebSocketMessage errorWebSocketMessage;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -30,5 +30,5 @@ import org.parceler.Parcel;
|
||||
@Parcel
|
||||
public class HelloResponseOverallWebSocketMessage extends BaseWebSocketMessage {
|
||||
@JsonField(name = "hello")
|
||||
HelloResponseWebSocketMessage helloResponseWebSocketMessage;
|
||||
public HelloResponseWebSocketMessage helloResponseWebSocketMessage;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -30,5 +30,5 @@ import org.parceler.Parcel;
|
||||
@Parcel
|
||||
public class JoinedRoomOverallWebSocketMessage extends BaseWebSocketMessage {
|
||||
@JsonField(name = "room")
|
||||
RoomWebSocketMessage roomWebSocketMessage;
|
||||
public RoomWebSocketMessage roomWebSocketMessage;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -31,5 +31,5 @@ import org.parceler.Parcel;
|
||||
@Data
|
||||
public class ServerHelloResponseFeaturesWebSocketMessage {
|
||||
@JsonField(name = "features")
|
||||
List<String> features;
|
||||
public List<String> features;
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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>
|
||||
}
|
@ -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?
|
||||
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user