mirror of
https://github.com/nextcloud/talk-android.git
synced 2024-11-29 18:08:58 +03:00
Merge pull request #1862 from nextcloud/bugfix/noid/kotlinConversion2
Migrate controllers to kotlin
This commit is contained in:
commit
d1d6898ffb
27 changed files with 1304 additions and 1210 deletions
|
@ -12,8 +12,8 @@ import java.util.Locale
|
||||||
class ShareUtilsIT {
|
class ShareUtilsIT {
|
||||||
@Test
|
@Test
|
||||||
fun date() {
|
fun date() {
|
||||||
assertEquals(1207778138000, parseDate2("Mon, 09 Apr 2008 23:55:38 GMT").time)
|
assertEquals(TEST_DATE_IN_MILLIS, parseDate2("Mon, 09 Apr 2008 23:55:38 GMT").time)
|
||||||
assertEquals(1207778138000, HttpUtils.parseDate("Mon, 09 Apr 2008 23:55:38 GMT")?.time)
|
assertEquals(TEST_DATE_IN_MILLIS, HttpUtils.parseDate("Mon, 09 Apr 2008 23:55:38 GMT")?.time)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseDate2(dateStr: String): Date {
|
private fun parseDate2(dateStr: String): Date {
|
||||||
|
@ -39,4 +39,8 @@ class ShareUtilsIT {
|
||||||
"EEE MMM d yyyy HH:mm:ss z"
|
"EEE MMM d yyyy HH:mm:ss z"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TEST_DATE_IN_MILLIS = 1207778138000
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,10 +81,6 @@ import javax.inject.Inject
|
||||||
@SuppressLint("LongLogTag")
|
@SuppressLint("LongLogTag")
|
||||||
@AutoInjector(NextcloudTalkApplication::class)
|
@AutoInjector(NextcloudTalkApplication::class)
|
||||||
class MagicFirebaseMessagingService : FirebaseMessagingService() {
|
class MagicFirebaseMessagingService : FirebaseMessagingService() {
|
||||||
companion object {
|
|
||||||
const val TAG = "MagicFirebaseMessagingService"
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmField
|
@JvmField
|
||||||
@Inject
|
@Inject
|
||||||
var appPreferences: AppPreferences? = null
|
var appPreferences: AppPreferences? = null
|
||||||
|
@ -162,6 +158,26 @@ class MagicFirebaseMessagingService : FirebaseMessagingService() {
|
||||||
base64DecodedSubject
|
base64DecodedSubject
|
||||||
)
|
)
|
||||||
if (signatureVerification!!.signatureValid) {
|
if (signatureVerification!!.signatureValid) {
|
||||||
|
decryptMessage(privateKey, base64DecodedSubject, subject, signature)
|
||||||
|
}
|
||||||
|
} catch (e1: NoSuchAlgorithmException) {
|
||||||
|
Log.d(NotificationWorker.TAG, "No proper algorithm to decrypt the message " + e1.localizedMessage)
|
||||||
|
} catch (e1: NoSuchPaddingException) {
|
||||||
|
Log.d(NotificationWorker.TAG, "No proper padding to decrypt the message " + e1.localizedMessage)
|
||||||
|
} catch (e1: InvalidKeyException) {
|
||||||
|
Log.d(NotificationWorker.TAG, "Invalid private key " + e1.localizedMessage)
|
||||||
|
}
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
Log.d(NotificationWorker.TAG, "Something went very wrong " + exception.localizedMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun decryptMessage(
|
||||||
|
privateKey: PrivateKey,
|
||||||
|
base64DecodedSubject: ByteArray?,
|
||||||
|
subject: String,
|
||||||
|
signature: String
|
||||||
|
) {
|
||||||
val cipher = Cipher.getInstance("RSA/None/PKCS1Padding")
|
val cipher = Cipher.getInstance("RSA/None/PKCS1Padding")
|
||||||
cipher.init(Cipher.DECRYPT_MODE, privateKey)
|
cipher.init(Cipher.DECRYPT_MODE, privateKey)
|
||||||
val decryptedSubject = cipher.doFinal(base64DecodedSubject)
|
val decryptedSubject = cipher.doFinal(base64DecodedSubject)
|
||||||
|
@ -241,17 +257,6 @@ class MagicFirebaseMessagingService : FirebaseMessagingService() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e1: NoSuchAlgorithmException) {
|
|
||||||
Log.d(NotificationWorker.TAG, "No proper algorithm to decrypt the message " + e1.localizedMessage)
|
|
||||||
} catch (e1: NoSuchPaddingException) {
|
|
||||||
Log.d(NotificationWorker.TAG, "No proper padding to decrypt the message " + e1.localizedMessage)
|
|
||||||
} catch (e1: InvalidKeyException) {
|
|
||||||
Log.d(NotificationWorker.TAG, "Invalid private key " + e1.localizedMessage)
|
|
||||||
}
|
|
||||||
} catch (exception: Exception) {
|
|
||||||
Log.d(NotificationWorker.TAG, "Something went very wrong " + exception.localizedMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkIfCallIsActive(
|
private fun checkIfCallIsActive(
|
||||||
signatureVerification: SignatureVerification,
|
signatureVerification: SignatureVerification,
|
||||||
|
@ -279,8 +284,8 @@ class MagicFirebaseMessagingService : FirebaseMessagingService() {
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
.repeatWhen { completed ->
|
.repeatWhen { completed ->
|
||||||
completed.zipWith(Observable.range(1, 12), { _, i -> i })
|
completed.zipWith(Observable.range(1, OBSERVABLE_COUNT), { _, i -> i })
|
||||||
.flatMap { Observable.timer(5, TimeUnit.SECONDS) }
|
.flatMap { Observable.timer(OBSERVABLE_DELAY, TimeUnit.SECONDS) }
|
||||||
.takeWhile { isServiceInForeground && hasParticipantsInCall && !inCallOnDifferentDevice }
|
.takeWhile { isServiceInForeground && hasParticipantsInCall && !inCallOnDifferentDevice }
|
||||||
}
|
}
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
|
@ -318,4 +323,10 @@ class MagicFirebaseMessagingService : FirebaseMessagingService() {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "MagicFirebaseMessagingService"
|
||||||
|
private const val OBSERVABLE_COUNT = 12
|
||||||
|
private const val OBSERVABLE_DELAY: Long = 5
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,11 @@
|
||||||
* Nextcloud Talk application
|
* Nextcloud Talk application
|
||||||
*
|
*
|
||||||
* @author Mario Danic
|
* @author Mario Danic
|
||||||
|
* @author Andy Scherzinger
|
||||||
* @author Marcel Hibbe
|
* @author Marcel Hibbe
|
||||||
* Copyright (C) 2017-2019 Mario Danic <mario@lovelyhq.com>
|
* Copyright (C) 2017-2019 Mario Danic <mario@lovelyhq.com>
|
||||||
* Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
|
* Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||||
|
* Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -97,9 +99,9 @@ class ClosedInterfaceImpl : ClosedInterface, ProviderInstaller.ProviderInstallLi
|
||||||
|
|
||||||
val periodicTokenRegistration = PeriodicWorkRequest.Builder(
|
val periodicTokenRegistration = PeriodicWorkRequest.Builder(
|
||||||
PushRegistrationWorker::class.java,
|
PushRegistrationWorker::class.java,
|
||||||
24,
|
DAILY,
|
||||||
TimeUnit.HOURS,
|
TimeUnit.HOURS,
|
||||||
10,
|
FLEX_INTERVAL,
|
||||||
TimeUnit.HOURS
|
TimeUnit.HOURS
|
||||||
)
|
)
|
||||||
.setInputData(data)
|
.setInputData(data)
|
||||||
|
@ -115,9 +117,9 @@ class ClosedInterfaceImpl : ClosedInterface, ProviderInstaller.ProviderInstallLi
|
||||||
private fun setUpPeriodicTokenRefreshFromFCM() {
|
private fun setUpPeriodicTokenRefreshFromFCM() {
|
||||||
val periodicTokenRefreshFromFCM = PeriodicWorkRequest.Builder(
|
val periodicTokenRefreshFromFCM = PeriodicWorkRequest.Builder(
|
||||||
GetFirebasePushTokenWorker::class.java,
|
GetFirebasePushTokenWorker::class.java,
|
||||||
30,
|
MONTHLY,
|
||||||
TimeUnit.DAYS,
|
TimeUnit.DAYS,
|
||||||
10,
|
FLEX_INTERVAL,
|
||||||
TimeUnit.DAYS,
|
TimeUnit.DAYS,
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
|
@ -128,4 +130,10 @@ class ClosedInterfaceImpl : ClosedInterface, ProviderInstaller.ProviderInstallLi
|
||||||
periodicTokenRefreshFromFCM
|
periodicTokenRefreshFromFCM
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val DAILY: Long = 24
|
||||||
|
const val MONTHLY: Long = 30
|
||||||
|
const val FLEX_INTERVAL: Long = 10
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -153,6 +153,18 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) : Message
|
||||||
showVoiceMessageLoading()
|
showVoiceMessageLoading()
|
||||||
WorkManager.getInstance(context!!).getWorkInfoByIdLiveData(workInfo.id)
|
WorkManager.getInstance(context!!).getWorkInfoByIdLiveData(workInfo.id)
|
||||||
.observeForever { info: WorkInfo? ->
|
.observeForever { info: WorkInfo? ->
|
||||||
|
showStatus(info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: ExecutionException) {
|
||||||
|
Log.e(TAG, "Error when checking if worker already exists", e)
|
||||||
|
} catch (e: InterruptedException) {
|
||||||
|
Log.e(TAG, "Error when checking if worker already exists", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showStatus(info: WorkInfo?) {
|
||||||
if (info != null) {
|
if (info != null) {
|
||||||
when (info.state) {
|
when (info.state) {
|
||||||
WorkInfo.State.RUNNING -> {
|
WorkInfo.State.RUNNING -> {
|
||||||
|
@ -172,14 +184,6 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) : Message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: ExecutionException) {
|
|
||||||
Log.e(TAG, "Error when checking if worker already exists", e)
|
|
||||||
} catch (e: InterruptedException) {
|
|
||||||
Log.e(TAG, "Error when checking if worker already exists", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showPlayButton() {
|
private fun showPlayButton() {
|
||||||
binding.playPauseBtn.visibility = View.VISIBLE
|
binding.playPauseBtn.visibility = View.VISIBLE
|
||||||
|
@ -203,6 +207,18 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) : Message
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!message.isGrouped && !message.isOneToOneConversation) {
|
if (!message.isGrouped && !message.isOneToOneConversation) {
|
||||||
|
setAvatarOnMessage(message)
|
||||||
|
} else {
|
||||||
|
if (message.isOneToOneConversation) {
|
||||||
|
binding.messageUserAvatar.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
binding.messageUserAvatar.visibility = View.INVISIBLE
|
||||||
|
}
|
||||||
|
binding.messageAuthor.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setAvatarOnMessage(message: ChatMessage) {
|
||||||
binding.messageUserAvatar.visibility = View.VISIBLE
|
binding.messageUserAvatar.visibility = View.VISIBLE
|
||||||
if (message.actorType == "guests") {
|
if (message.actorType == "guests") {
|
||||||
// do nothing, avatar is set
|
// do nothing, avatar is set
|
||||||
|
@ -228,14 +244,6 @@ class IncomingVoiceMessageViewHolder(incomingView: View, payload: Any) : Message
|
||||||
binding.messageUserAvatar.visibility = View.VISIBLE
|
binding.messageUserAvatar.visibility = View.VISIBLE
|
||||||
binding.messageUserAvatar.setImageDrawable(drawable)
|
binding.messageUserAvatar.setImageDrawable(drawable)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
if (message.isOneToOneConversation) {
|
|
||||||
binding.messageUserAvatar.visibility = View.GONE
|
|
||||||
} else {
|
|
||||||
binding.messageUserAvatar.visibility = View.INVISIBLE
|
|
||||||
}
|
|
||||||
binding.messageAuthor.visibility = View.GONE
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun colorizeMessageBubble(message: ChatMessage) {
|
private fun colorizeMessageBubble(message: ChatMessage) {
|
||||||
|
|
|
@ -26,6 +26,7 @@ package com.nextcloud.talk.adapters.messages
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.res.Resources
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.graphics.drawable.LayerDrawable
|
import android.graphics.drawable.LayerDrawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
@ -53,6 +54,7 @@ import com.nextcloud.talk.utils.DisplayUtils
|
||||||
import com.nextcloud.talk.utils.TextMatchers
|
import com.nextcloud.talk.utils.TextMatchers
|
||||||
import com.nextcloud.talk.utils.preferences.AppPreferences
|
import com.nextcloud.talk.utils.preferences.AppPreferences
|
||||||
import com.stfalcon.chatkit.messages.MessageHolders
|
import com.stfalcon.chatkit.messages.MessageHolders
|
||||||
|
import java.util.HashMap
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AutoInjector(NextcloudTalkApplication::class)
|
@AutoInjector(NextcloudTalkApplication::class)
|
||||||
|
@ -72,17 +74,117 @@ class MagicIncomingTextMessageViewHolder(itemView: View, payload: Any) : Message
|
||||||
override fun onBind(message: ChatMessage) {
|
override fun onBind(message: ChatMessage) {
|
||||||
super.onBind(message)
|
super.onBind(message)
|
||||||
sharedApplication!!.componentApplication.inject(this)
|
sharedApplication!!.componentApplication.inject(this)
|
||||||
val author: String = message.actorDisplayName
|
processAuthor(message)
|
||||||
if (!TextUtils.isEmpty(author)) {
|
|
||||||
binding.messageAuthor.text = author
|
if (!message.isGrouped && !message.isOneToOneConversation) {
|
||||||
|
showAvatarOnChatMessage(message)
|
||||||
|
} else {
|
||||||
|
if (message.isOneToOneConversation) {
|
||||||
|
binding.messageUserAvatar.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
binding.messageUserAvatar.visibility = View.INVISIBLE
|
||||||
|
}
|
||||||
|
binding.messageAuthor.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
val resources = itemView.resources
|
||||||
|
|
||||||
|
setBubbleOnChatMessage(message, resources)
|
||||||
|
|
||||||
|
itemView.isSelected = false
|
||||||
|
binding.messageTime.setTextColor(ResourcesCompat.getColor(resources, R.color.warm_grey_four, null))
|
||||||
|
|
||||||
|
var messageString: Spannable = SpannableString(message.text)
|
||||||
|
|
||||||
|
var textSize = context?.resources!!.getDimension(R.dimen.chat_text_size)
|
||||||
|
|
||||||
|
val messageParameters = message.messageParameters
|
||||||
|
if (messageParameters != null && messageParameters.size > 0) {
|
||||||
|
messageString = processMessageParameters(messageParameters, message, messageString)
|
||||||
|
} else if (TextMatchers.isMessageWithSingleEmoticonOnly(message.text)) {
|
||||||
|
textSize = (textSize * TEXT_SIZE_MULTIPLIER).toFloat()
|
||||||
|
itemView.isSelected = true
|
||||||
|
binding.messageAuthor.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.messageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
|
||||||
|
binding.messageText.text = messageString
|
||||||
|
|
||||||
|
// parent message handling
|
||||||
|
if (!message.isDeleted && message.parentMessage != null) {
|
||||||
|
processParentMessage(message)
|
||||||
|
binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
binding.messageQuote.quotedChatMessageView.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
itemView.setTag(MessageSwipeCallback.REPLYABLE_VIEW_TAG, message.isReplyable)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processAuthor(message: ChatMessage) {
|
||||||
|
if (!TextUtils.isEmpty(message.actorDisplayName)) {
|
||||||
|
binding.messageAuthor.text = message.actorDisplayName
|
||||||
binding.messageUserAvatar.setOnClickListener {
|
binding.messageUserAvatar.setOnClickListener {
|
||||||
(payload as? ProfileBottomSheet)?.showFor(message.actorId, itemView.context)
|
(payload as? ProfileBottomSheet)?.showFor(message.actorId, itemView.context)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
binding.messageAuthor.setText(R.string.nc_nick_guest)
|
binding.messageAuthor.setText(R.string.nc_nick_guest)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!message.isGrouped && !message.isOneToOneConversation) {
|
private fun setBubbleOnChatMessage(
|
||||||
|
message: ChatMessage,
|
||||||
|
resources: Resources
|
||||||
|
) {
|
||||||
|
val bgBubbleColor = if (message.isDeleted) {
|
||||||
|
ResourcesCompat.getColor(resources, R.color.bg_message_list_incoming_bubble_deleted, null)
|
||||||
|
} else {
|
||||||
|
ResourcesCompat.getColor(resources, R.color.bg_message_list_incoming_bubble, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
var bubbleResource = R.drawable.shape_incoming_message
|
||||||
|
|
||||||
|
if (message.isGrouped) {
|
||||||
|
bubbleResource = R.drawable.shape_grouped_incoming_message
|
||||||
|
}
|
||||||
|
|
||||||
|
val bubbleDrawable = DisplayUtils.getMessageSelector(
|
||||||
|
bgBubbleColor,
|
||||||
|
ResourcesCompat.getColor(resources, R.color.transparent, null),
|
||||||
|
bgBubbleColor, bubbleResource
|
||||||
|
)
|
||||||
|
ViewCompat.setBackground(bubble, bubbleDrawable)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processParentMessage(message: ChatMessage) {
|
||||||
|
val parentChatMessage = message.parentMessage
|
||||||
|
parentChatMessage.activeUser = message.activeUser
|
||||||
|
parentChatMessage.imageUrl?.let {
|
||||||
|
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
|
||||||
|
binding.messageQuote.quotedMessageImage.load(it) {
|
||||||
|
addHeader(
|
||||||
|
"Authorization",
|
||||||
|
ApiUtils.getCredentials(message.activeUser.username, message.activeUser.token)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} ?: run {
|
||||||
|
binding.messageQuote.quotedMessageImage.visibility = View.GONE
|
||||||
|
}
|
||||||
|
binding.messageQuote.quotedMessageAuthor.text = if (parentChatMessage.actorDisplayName.isNullOrEmpty())
|
||||||
|
context!!.getText(R.string.nc_nick_guest) else parentChatMessage.actorDisplayName
|
||||||
|
binding.messageQuote.quotedMessage.text = parentChatMessage.text
|
||||||
|
|
||||||
|
binding.messageQuote.quotedMessageAuthor
|
||||||
|
.setTextColor(ContextCompat.getColor(context!!, R.color.textColorMaxContrast))
|
||||||
|
|
||||||
|
if (parentChatMessage.actorId?.equals(message.activeUser.userId) == true) {
|
||||||
|
binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.colorPrimary)
|
||||||
|
} else {
|
||||||
|
binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.textColorMaxContrast)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showAvatarOnChatMessage(message: ChatMessage) {
|
||||||
binding.messageUserAvatar.visibility = View.VISIBLE
|
binding.messageUserAvatar.visibility = View.VISIBLE
|
||||||
if (message.actorType == "guests") {
|
if (message.actorType == "guests") {
|
||||||
// do nothing, avatar is set
|
// do nothing, avatar is set
|
||||||
|
@ -110,46 +212,14 @@ class MagicIncomingTextMessageViewHolder(itemView: View, payload: Any) : Message
|
||||||
binding.messageUserAvatar.visibility = View.VISIBLE
|
binding.messageUserAvatar.visibility = View.VISIBLE
|
||||||
binding.messageUserAvatar.setImageDrawable(drawable)
|
binding.messageUserAvatar.setImageDrawable(drawable)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
if (message.isOneToOneConversation) {
|
|
||||||
binding.messageUserAvatar.visibility = View.GONE
|
|
||||||
} else {
|
|
||||||
binding.messageUserAvatar.visibility = View.INVISIBLE
|
|
||||||
}
|
|
||||||
binding.messageAuthor.visibility = View.GONE
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val resources = itemView.resources
|
private fun processMessageParameters(
|
||||||
|
messageParameters: HashMap<String, HashMap<String, String>>,
|
||||||
val bgBubbleColor = if (message.isDeleted) {
|
message: ChatMessage,
|
||||||
ResourcesCompat.getColor(resources, R.color.bg_message_list_incoming_bubble_deleted, null)
|
messageString: Spannable
|
||||||
} else {
|
): Spannable {
|
||||||
ResourcesCompat.getColor(resources, R.color.bg_message_list_incoming_bubble, null)
|
var messageStringInternal = messageString
|
||||||
}
|
|
||||||
|
|
||||||
var bubbleResource = R.drawable.shape_incoming_message
|
|
||||||
|
|
||||||
if (message.isGrouped) {
|
|
||||||
bubbleResource = R.drawable.shape_grouped_incoming_message
|
|
||||||
}
|
|
||||||
|
|
||||||
val bubbleDrawable = DisplayUtils.getMessageSelector(
|
|
||||||
bgBubbleColor,
|
|
||||||
ResourcesCompat.getColor(resources, R.color.transparent, null),
|
|
||||||
bgBubbleColor, bubbleResource
|
|
||||||
)
|
|
||||||
ViewCompat.setBackground(bubble, bubbleDrawable)
|
|
||||||
|
|
||||||
val messageParameters = message.messageParameters
|
|
||||||
|
|
||||||
itemView.isSelected = false
|
|
||||||
binding.messageTime.setTextColor(ResourcesCompat.getColor(resources, R.color.warm_grey_four, null))
|
|
||||||
|
|
||||||
var messageString: Spannable = SpannableString(message.text)
|
|
||||||
|
|
||||||
var textSize = context?.resources!!.getDimension(R.dimen.chat_text_size)
|
|
||||||
|
|
||||||
if (messageParameters != null && messageParameters.size > 0) {
|
|
||||||
for (key in messageParameters.keys) {
|
for (key in messageParameters.keys) {
|
||||||
val individualHashMap = message.messageParameters[key]
|
val individualHashMap = message.messageParameters[key]
|
||||||
if (individualHashMap != null) {
|
if (individualHashMap != null) {
|
||||||
|
@ -159,9 +229,9 @@ class MagicIncomingTextMessageViewHolder(itemView: View, payload: Any) : Message
|
||||||
individualHashMap["type"] == "call"
|
individualHashMap["type"] == "call"
|
||||||
) {
|
) {
|
||||||
if (individualHashMap["id"] == message.activeUser!!.userId) {
|
if (individualHashMap["id"] == message.activeUser!!.userId) {
|
||||||
messageString = DisplayUtils.searchAndReplaceWithMentionSpan(
|
messageStringInternal = DisplayUtils.searchAndReplaceWithMentionSpan(
|
||||||
binding.messageText.context,
|
binding.messageText.context,
|
||||||
messageString,
|
messageStringInternal,
|
||||||
individualHashMap["id"]!!,
|
individualHashMap["id"]!!,
|
||||||
individualHashMap["name"]!!,
|
individualHashMap["name"]!!,
|
||||||
individualHashMap["type"]!!,
|
individualHashMap["type"]!!,
|
||||||
|
@ -169,9 +239,9 @@ class MagicIncomingTextMessageViewHolder(itemView: View, payload: Any) : Message
|
||||||
R.xml.chip_you
|
R.xml.chip_you
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
messageString = DisplayUtils.searchAndReplaceWithMentionSpan(
|
messageStringInternal = DisplayUtils.searchAndReplaceWithMentionSpan(
|
||||||
binding.messageText.context,
|
binding.messageText.context,
|
||||||
messageString,
|
messageStringInternal,
|
||||||
individualHashMap["id"]!!,
|
individualHashMap["id"]!!,
|
||||||
individualHashMap["name"]!!,
|
individualHashMap["name"]!!,
|
||||||
individualHashMap["type"]!!,
|
individualHashMap["type"]!!,
|
||||||
|
@ -187,48 +257,10 @@ class MagicIncomingTextMessageViewHolder(itemView: View, payload: Any) : Message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (TextMatchers.isMessageWithSingleEmoticonOnly(message.text)) {
|
return messageStringInternal
|
||||||
textSize = (textSize * 2.5).toFloat()
|
|
||||||
itemView.isSelected = true
|
|
||||||
binding.messageAuthor.visibility = View.GONE
|
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.messageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
|
companion object {
|
||||||
binding.messageText.text = messageString
|
const val TEXT_SIZE_MULTIPLIER = 2.5
|
||||||
|
|
||||||
// parent message handling
|
|
||||||
if (!message.isDeleted && message.parentMessage != null) {
|
|
||||||
val parentChatMessage = message.parentMessage
|
|
||||||
parentChatMessage.activeUser = message.activeUser
|
|
||||||
parentChatMessage.imageUrl?.let {
|
|
||||||
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
|
|
||||||
binding.messageQuote.quotedMessageImage.load(it) {
|
|
||||||
addHeader(
|
|
||||||
"Authorization",
|
|
||||||
ApiUtils.getCredentials(message.activeUser.username, message.activeUser.token)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} ?: run {
|
|
||||||
binding.messageQuote.quotedMessageImage.visibility = View.GONE
|
|
||||||
}
|
|
||||||
binding.messageQuote.quotedMessageAuthor.text = if (parentChatMessage.actorDisplayName.isNullOrEmpty())
|
|
||||||
context!!.getText(R.string.nc_nick_guest) else parentChatMessage.actorDisplayName
|
|
||||||
binding.messageQuote.quotedMessage.text = parentChatMessage.text
|
|
||||||
|
|
||||||
binding.messageQuote.quotedMessageAuthor
|
|
||||||
.setTextColor(ContextCompat.getColor(context!!, R.color.textColorMaxContrast))
|
|
||||||
|
|
||||||
if (parentChatMessage.actorId?.equals(message.activeUser.userId) == true) {
|
|
||||||
binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.colorPrimary)
|
|
||||||
} else {
|
|
||||||
binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.textColorMaxContrast)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
|
|
||||||
} else {
|
|
||||||
binding.messageQuote.quotedChatMessageView.visibility = View.GONE
|
|
||||||
}
|
|
||||||
|
|
||||||
itemView.setTag(MessageSwipeCallback.REPLYABLE_VIEW_TAG, message.isReplyable)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,91 +72,25 @@ class MagicOutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessage
|
||||||
layoutParams.isWrapBefore = false
|
layoutParams.isWrapBefore = false
|
||||||
var textSize = context!!.resources.getDimension(R.dimen.chat_text_size)
|
var textSize = context!!.resources.getDimension(R.dimen.chat_text_size)
|
||||||
if (messageParameters != null && messageParameters.size > 0) {
|
if (messageParameters != null && messageParameters.size > 0) {
|
||||||
for (key in messageParameters.keys) {
|
messageString = processMessageParameters(messageParameters, message, messageString)
|
||||||
val individualHashMap: HashMap<String, String>? = message.messageParameters[key]
|
|
||||||
if (individualHashMap != null) {
|
|
||||||
if (individualHashMap["type"] == "user" ||
|
|
||||||
individualHashMap["type"] == "guest" ||
|
|
||||||
individualHashMap["type"] == "call"
|
|
||||||
) {
|
|
||||||
messageString = searchAndReplaceWithMentionSpan(
|
|
||||||
binding.messageText.context,
|
|
||||||
messageString,
|
|
||||||
individualHashMap["id"]!!,
|
|
||||||
individualHashMap["name"]!!,
|
|
||||||
individualHashMap["type"]!!,
|
|
||||||
message.activeUser,
|
|
||||||
R.xml.chip_others
|
|
||||||
)
|
|
||||||
} else if (individualHashMap["type"] == "file") {
|
|
||||||
realView.setOnClickListener { v: View? ->
|
|
||||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(individualHashMap["link"]))
|
|
||||||
context!!.startActivity(browserIntent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (TextMatchers.isMessageWithSingleEmoticonOnly(message.text)) {
|
} else if (TextMatchers.isMessageWithSingleEmoticonOnly(message.text)) {
|
||||||
textSize = (textSize * 2.5).toFloat()
|
textSize = (textSize * TEXT_SIZE_MULTIPLIER).toFloat()
|
||||||
layoutParams.isWrapBefore = true
|
layoutParams.isWrapBefore = true
|
||||||
binding.messageTime.setTextColor(
|
binding.messageTime.setTextColor(
|
||||||
ResourcesCompat.getColor(context!!.resources, R.color.warm_grey_four, null)
|
ResourcesCompat.getColor(context!!.resources, R.color.warm_grey_four, null)
|
||||||
)
|
)
|
||||||
realView.isSelected = true
|
realView.isSelected = true
|
||||||
}
|
}
|
||||||
val resources = sharedApplication!!.resources
|
|
||||||
val bgBubbleColor = if (message.isDeleted) {
|
setBubbleOnChatMessage(message)
|
||||||
ResourcesCompat.getColor(resources, R.color.bg_message_list_outcoming_bubble_deleted, null)
|
|
||||||
} else {
|
|
||||||
ResourcesCompat.getColor(resources, R.color.bg_message_list_outcoming_bubble, null)
|
|
||||||
}
|
|
||||||
if (message.isGrouped) {
|
|
||||||
val bubbleDrawable = getMessageSelector(
|
|
||||||
bgBubbleColor,
|
|
||||||
ResourcesCompat.getColor(resources, R.color.transparent, null),
|
|
||||||
bgBubbleColor,
|
|
||||||
R.drawable.shape_grouped_outcoming_message
|
|
||||||
)
|
|
||||||
ViewCompat.setBackground(bubble, bubbleDrawable)
|
|
||||||
} else {
|
|
||||||
val bubbleDrawable = getMessageSelector(
|
|
||||||
bgBubbleColor,
|
|
||||||
ResourcesCompat.getColor(resources, R.color.transparent, null),
|
|
||||||
bgBubbleColor,
|
|
||||||
R.drawable.shape_outcoming_message
|
|
||||||
)
|
|
||||||
ViewCompat.setBackground(bubble, bubbleDrawable)
|
|
||||||
}
|
|
||||||
binding.messageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
|
binding.messageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
|
||||||
binding.messageTime.layoutParams = layoutParams
|
binding.messageTime.layoutParams = layoutParams
|
||||||
binding.messageText.text = messageString
|
binding.messageText.text = messageString
|
||||||
|
|
||||||
// parent message handling
|
// parent message handling
|
||||||
|
|
||||||
if (!message.isDeleted && message.parentMessage != null) {
|
if (!message.isDeleted && message.parentMessage != null) {
|
||||||
val parentChatMessage = message.parentMessage
|
processParentMessage(message)
|
||||||
parentChatMessage.activeUser = message.activeUser
|
|
||||||
parentChatMessage.imageUrl?.let {
|
|
||||||
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
|
|
||||||
binding.messageQuote.quotedMessageImage.load(it) {
|
|
||||||
addHeader(
|
|
||||||
"Authorization",
|
|
||||||
ApiUtils.getCredentials(message.activeUser.username, message.activeUser.token)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} ?: run {
|
|
||||||
binding.messageQuote.quotedMessageImage.visibility = View.GONE
|
|
||||||
}
|
|
||||||
binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
|
|
||||||
?: context!!.getText(R.string.nc_nick_guest)
|
|
||||||
binding.messageQuote.quotedMessage.text = parentChatMessage.text
|
|
||||||
binding.messageQuote.quotedMessage.setTextColor(
|
|
||||||
ContextCompat.getColor(context!!, R.color.nc_outcoming_text_default)
|
|
||||||
)
|
|
||||||
binding.messageQuote.quotedMessageAuthor.setTextColor(ContextCompat.getColor(context!!, R.color.nc_grey))
|
|
||||||
|
|
||||||
binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.white)
|
|
||||||
|
|
||||||
binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
|
binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
binding.messageQuote.quotedChatMessageView.visibility = View.GONE
|
binding.messageQuote.quotedChatMessageView.visibility = View.GONE
|
||||||
|
@ -185,4 +119,92 @@ class MagicOutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessage
|
||||||
|
|
||||||
itemView.setTag(MessageSwipeCallback.REPLYABLE_VIEW_TAG, message.isReplyable)
|
itemView.setTag(MessageSwipeCallback.REPLYABLE_VIEW_TAG, message.isReplyable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun processParentMessage(message: ChatMessage) {
|
||||||
|
val parentChatMessage = message.parentMessage
|
||||||
|
parentChatMessage.activeUser = message.activeUser
|
||||||
|
parentChatMessage.imageUrl?.let {
|
||||||
|
binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE
|
||||||
|
binding.messageQuote.quotedMessageImage.load(it) {
|
||||||
|
addHeader(
|
||||||
|
"Authorization",
|
||||||
|
ApiUtils.getCredentials(message.activeUser.username, message.activeUser.token)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} ?: run {
|
||||||
|
binding.messageQuote.quotedMessageImage.visibility = View.GONE
|
||||||
|
}
|
||||||
|
binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName
|
||||||
|
?: context!!.getText(R.string.nc_nick_guest)
|
||||||
|
binding.messageQuote.quotedMessage.text = parentChatMessage.text
|
||||||
|
binding.messageQuote.quotedMessage.setTextColor(
|
||||||
|
ContextCompat.getColor(context!!, R.color.nc_outcoming_text_default)
|
||||||
|
)
|
||||||
|
binding.messageQuote.quotedMessageAuthor.setTextColor(ContextCompat.getColor(context!!, R.color.nc_grey))
|
||||||
|
|
||||||
|
binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.white)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setBubbleOnChatMessage(message: ChatMessage) {
|
||||||
|
val resources = sharedApplication!!.resources
|
||||||
|
val bgBubbleColor = if (message.isDeleted) {
|
||||||
|
ResourcesCompat.getColor(resources, R.color.bg_message_list_outcoming_bubble_deleted, null)
|
||||||
|
} else {
|
||||||
|
ResourcesCompat.getColor(resources, R.color.bg_message_list_outcoming_bubble, null)
|
||||||
|
}
|
||||||
|
if (message.isGrouped) {
|
||||||
|
val bubbleDrawable = getMessageSelector(
|
||||||
|
bgBubbleColor,
|
||||||
|
ResourcesCompat.getColor(resources, R.color.transparent, null),
|
||||||
|
bgBubbleColor,
|
||||||
|
R.drawable.shape_grouped_outcoming_message
|
||||||
|
)
|
||||||
|
ViewCompat.setBackground(bubble, bubbleDrawable)
|
||||||
|
} else {
|
||||||
|
val bubbleDrawable = getMessageSelector(
|
||||||
|
bgBubbleColor,
|
||||||
|
ResourcesCompat.getColor(resources, R.color.transparent, null),
|
||||||
|
bgBubbleColor,
|
||||||
|
R.drawable.shape_outcoming_message
|
||||||
|
)
|
||||||
|
ViewCompat.setBackground(bubble, bubbleDrawable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processMessageParameters(
|
||||||
|
messageParameters: HashMap<String, HashMap<String, String>>,
|
||||||
|
message: ChatMessage,
|
||||||
|
messageString: Spannable
|
||||||
|
): Spannable {
|
||||||
|
var messageString1 = messageString
|
||||||
|
for (key in messageParameters.keys) {
|
||||||
|
val individualHashMap: HashMap<String, String>? = message.messageParameters[key]
|
||||||
|
if (individualHashMap != null) {
|
||||||
|
if (individualHashMap["type"] == "user" ||
|
||||||
|
individualHashMap["type"] == "guest" ||
|
||||||
|
individualHashMap["type"] == "call"
|
||||||
|
) {
|
||||||
|
messageString1 = searchAndReplaceWithMentionSpan(
|
||||||
|
binding.messageText.context,
|
||||||
|
messageString1,
|
||||||
|
individualHashMap["id"]!!,
|
||||||
|
individualHashMap["name"]!!,
|
||||||
|
individualHashMap["type"]!!,
|
||||||
|
message.activeUser,
|
||||||
|
R.xml.chip_others
|
||||||
|
)
|
||||||
|
} else if (individualHashMap["type"] == "file") {
|
||||||
|
realView.setOnClickListener { v: View? ->
|
||||||
|
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(individualHashMap["link"]))
|
||||||
|
context!!.startActivity(browserIntent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return messageString1
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TEXT_SIZE_MULTIPLIER = 2.5
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,36 +87,11 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) : MessageHolders
|
||||||
updateDownloadState(message)
|
updateDownloadState(message)
|
||||||
binding.seekbar.max = message.voiceMessageDuration
|
binding.seekbar.max = message.voiceMessageDuration
|
||||||
|
|
||||||
if (message.isPlayingVoiceMessage) {
|
handleIsPlayingVoiceMessageState(message)
|
||||||
showPlayButton()
|
|
||||||
binding.playPauseBtn.icon = ContextCompat.getDrawable(
|
|
||||||
context!!,
|
|
||||||
R.drawable.ic_baseline_pause_voice_message_24
|
|
||||||
)
|
|
||||||
binding.seekbar.progress = message.voiceMessagePlayedSeconds
|
|
||||||
} else {
|
|
||||||
binding.playPauseBtn.visibility = View.VISIBLE
|
|
||||||
binding.playPauseBtn.icon = ContextCompat.getDrawable(
|
|
||||||
context!!,
|
|
||||||
R.drawable.ic_baseline_play_arrow_voice_message_24
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.isDownloadingVoiceMessage) {
|
handleIsDownloadingVoiceMessageState(message)
|
||||||
showVoiceMessageLoading()
|
|
||||||
} else {
|
|
||||||
binding.progressBar.visibility = View.GONE
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.resetVoiceMessage) {
|
handleResetVoiceMessageState(message)
|
||||||
binding.playPauseBtn.visibility = View.VISIBLE
|
|
||||||
binding.playPauseBtn.icon = ContextCompat.getDrawable(
|
|
||||||
context!!,
|
|
||||||
R.drawable.ic_baseline_play_arrow_voice_message_24
|
|
||||||
)
|
|
||||||
binding.seekbar.progress = SEEKBAR_START
|
|
||||||
message.resetVoiceMessage = false
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.seekbar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
binding.seekbar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
||||||
override fun onStopTrackingTouch(seekBar: SeekBar) {
|
override fun onStopTrackingTouch(seekBar: SeekBar) {
|
||||||
|
@ -156,6 +131,43 @@ class OutcomingVoiceMessageViewHolder(outcomingView: View) : MessageHolders
|
||||||
binding.checkMark.setContentDescription(readStatusContentDescriptionString)
|
binding.checkMark.setContentDescription(readStatusContentDescriptionString)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleResetVoiceMessageState(message: ChatMessage) {
|
||||||
|
if (message.resetVoiceMessage) {
|
||||||
|
binding.playPauseBtn.visibility = View.VISIBLE
|
||||||
|
binding.playPauseBtn.icon = ContextCompat.getDrawable(
|
||||||
|
context!!,
|
||||||
|
R.drawable.ic_baseline_play_arrow_voice_message_24
|
||||||
|
)
|
||||||
|
binding.seekbar.progress = SEEKBAR_START
|
||||||
|
message.resetVoiceMessage = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleIsDownloadingVoiceMessageState(message: ChatMessage) {
|
||||||
|
if (message.isDownloadingVoiceMessage) {
|
||||||
|
showVoiceMessageLoading()
|
||||||
|
} else {
|
||||||
|
binding.progressBar.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleIsPlayingVoiceMessageState(message: ChatMessage) {
|
||||||
|
if (message.isPlayingVoiceMessage) {
|
||||||
|
showPlayButton()
|
||||||
|
binding.playPauseBtn.icon = ContextCompat.getDrawable(
|
||||||
|
context!!,
|
||||||
|
R.drawable.ic_baseline_pause_voice_message_24
|
||||||
|
)
|
||||||
|
binding.seekbar.progress = message.voiceMessagePlayedSeconds
|
||||||
|
} else {
|
||||||
|
binding.playPauseBtn.visibility = View.VISIBLE
|
||||||
|
binding.playPauseBtn.icon = ContextCompat.getDrawable(
|
||||||
|
context!!,
|
||||||
|
R.drawable.ic_baseline_play_arrow_voice_message_24
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun updateDownloadState(message: ChatMessage) {
|
private fun updateDownloadState(message: ChatMessage) {
|
||||||
// check if download worker is already running
|
// check if download worker is already running
|
||||||
val fileId = message.getSelectedIndividualHashMap()["id"]
|
val fileId = message.getSelectedIndividualHashMap()["id"]
|
||||||
|
|
|
@ -2,10 +2,12 @@
|
||||||
*
|
*
|
||||||
* Nextcloud Talk application
|
* Nextcloud Talk application
|
||||||
*
|
*
|
||||||
* @author Mario Danic
|
|
||||||
* @author Marcel Hibbe
|
* @author Marcel Hibbe
|
||||||
* Copyright (C) 2017 Mario Danic <mario@lovelyhq.com>
|
* @author Andy Scherzinger
|
||||||
|
* @author Mario Danic
|
||||||
* Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
|
* Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||||
|
* Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
|
||||||
|
* Copyright (C) 2017 Mario Danic <mario@lovelyhq.com>
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -170,7 +172,7 @@ class NextcloudTalkApplication : MultiDexApplication(), LifecycleObserver {
|
||||||
val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java).build()
|
val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java).build()
|
||||||
val periodicCapabilitiesUpdateWork = PeriodicWorkRequest.Builder(
|
val periodicCapabilitiesUpdateWork = PeriodicWorkRequest.Builder(
|
||||||
CapabilitiesWorker::class.java,
|
CapabilitiesWorker::class.java,
|
||||||
12, TimeUnit.HOURS
|
HALF_DAY, TimeUnit.HOURS
|
||||||
).build()
|
).build()
|
||||||
val capabilitiesUpdateWork = OneTimeWorkRequest.Builder(CapabilitiesWorker::class.java).build()
|
val capabilitiesUpdateWork = OneTimeWorkRequest.Builder(CapabilitiesWorker::class.java).build()
|
||||||
val signalingSettingsWork = OneTimeWorkRequest.Builder(SignalingSettingsWorker::class.java).build()
|
val signalingSettingsWork = OneTimeWorkRequest.Builder(SignalingSettingsWorker::class.java).build()
|
||||||
|
@ -218,7 +220,7 @@ class NextcloudTalkApplication : MultiDexApplication(), LifecycleObserver {
|
||||||
|
|
||||||
private fun buildDefaultImageLoader(): ImageLoader {
|
private fun buildDefaultImageLoader(): ImageLoader {
|
||||||
return ImageLoader.Builder(applicationContext)
|
return ImageLoader.Builder(applicationContext)
|
||||||
.availableMemoryPercentage(0.5) // Use 50% of the application's available memory.
|
.availableMemoryPercentage(FIFTY_PERCENT) // Use 50% of the application's available memory.
|
||||||
.crossfade(true) // Show a short crossfade when loading images from network or disk into an ImageView.
|
.crossfade(true) // Show a short crossfade when loading images from network or disk into an ImageView.
|
||||||
.componentRegistry {
|
.componentRegistry {
|
||||||
if (SDK_INT >= P) {
|
if (SDK_INT >= P) {
|
||||||
|
@ -234,6 +236,8 @@ class NextcloudTalkApplication : MultiDexApplication(), LifecycleObserver {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = NextcloudTalkApplication::class.java.simpleName
|
private val TAG = NextcloudTalkApplication::class.java.simpleName
|
||||||
|
const val FIFTY_PERCENT = 0.5
|
||||||
|
const val HALF_DAY: Long = 12
|
||||||
//region Singleton
|
//region Singleton
|
||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
* Nextcloud Talk application
|
* Nextcloud Talk application
|
||||||
*
|
*
|
||||||
* @author Mario Danic
|
* @author Mario Danic
|
||||||
|
* @author Andy Scherzinger
|
||||||
|
* Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
|
||||||
* Copyright (C) 2017 Mario Danic (mario@lovelyhq.com)
|
* Copyright (C) 2017 Mario Danic (mario@lovelyhq.com)
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
@ -161,7 +163,9 @@ class AccountVerificationController(args: Bundle? = null) :
|
||||||
RouterTransaction.with(
|
RouterTransaction.with(
|
||||||
WebViewLoginController(
|
WebViewLoginController(
|
||||||
baseUrl,
|
baseUrl,
|
||||||
false, username, ""
|
false,
|
||||||
|
username,
|
||||||
|
""
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.pushChangeHandler(HorizontalChangeHandler())
|
.pushChangeHandler(HorizontalChangeHandler())
|
||||||
|
@ -473,7 +477,7 @@ class AccountVerificationController(args: Bundle? = null) :
|
||||||
// unused atm
|
// unused atm
|
||||||
}
|
}
|
||||||
override fun onComplete() {
|
override fun onComplete() {
|
||||||
activity?.runOnUiThread { Handler().postDelayed({ router.popToRoot() }, 7500) }
|
activity?.runOnUiThread { Handler().postDelayed({ router.popToRoot() }, DELAY_IN_MILLIS) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onError(e: Throwable) {
|
override fun onError(e: Throwable) {
|
||||||
|
@ -481,7 +485,7 @@ class AccountVerificationController(args: Bundle? = null) :
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
activity?.runOnUiThread { Handler().postDelayed({ router.popToRoot() }, 7500) }
|
activity?.runOnUiThread { Handler().postDelayed({ router.popToRoot() }, DELAY_IN_MILLIS) }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ApplicationWideMessageHolder.getInstance().setMessageType(
|
ApplicationWideMessageHolder.getInstance().setMessageType(
|
||||||
|
@ -508,13 +512,14 @@ class AccountVerificationController(args: Bundle? = null) :
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 7500)
|
}, DELAY_IN_MILLIS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = "AccountVerificationController"
|
const val TAG = "AccountVerificationController"
|
||||||
|
const val DELAY_IN_MILLIS: Long = 7500
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
|
@ -423,7 +423,7 @@ class ChatController(args: Bundle) :
|
||||||
override fun onNewResultImpl(bitmap: Bitmap?) {
|
override fun onNewResultImpl(bitmap: Bitmap?) {
|
||||||
if (actionBar != null && bitmap != null && resources != null) {
|
if (actionBar != null && bitmap != null && resources != null) {
|
||||||
|
|
||||||
val avatarSize = (actionBar?.height!! / 1.5).roundToInt()
|
val avatarSize = (actionBar?.height!! / TOOLBAR_AVATAR_RATIO).roundToInt()
|
||||||
if (avatarSize > 0) {
|
if (avatarSize > 0) {
|
||||||
val bitmapResized = Bitmap.createScaledBitmap(bitmap, avatarSize, avatarSize, false)
|
val bitmapResized = Bitmap.createScaledBitmap(bitmap, avatarSize, avatarSize, false)
|
||||||
|
|
||||||
|
@ -1043,7 +1043,7 @@ class ChatController(args: Bundle) :
|
||||||
binding.messageInputView.audioRecordDuration.start()
|
binding.messageInputView.audioRecordDuration.start()
|
||||||
|
|
||||||
val animation: Animation = AlphaAnimation(1.0f, 0.0f)
|
val animation: Animation = AlphaAnimation(1.0f, 0.0f)
|
||||||
animation.duration = 750
|
animation.duration = ANIMATION_DURATION
|
||||||
animation.interpolator = LinearInterpolator()
|
animation.interpolator = LinearInterpolator()
|
||||||
animation.repeatCount = Animation.INFINITE
|
animation.repeatCount = Animation.INFINITE
|
||||||
animation.repeatMode = Animation.REVERSE
|
animation.repeatMode = Animation.REVERSE
|
||||||
|
@ -1494,7 +1494,7 @@ class ChatController(args: Bundle) :
|
||||||
|
|
||||||
private fun setupMentionAutocomplete() {
|
private fun setupMentionAutocomplete() {
|
||||||
if (isAlive()) {
|
if (isAlive()) {
|
||||||
val elevation = 6f
|
val elevation = MENTION_AUTO_COMPLETE_ELEVATION
|
||||||
resources?.let {
|
resources?.let {
|
||||||
val backgroundDrawable = ColorDrawable(it.getColor(R.color.bg_default))
|
val backgroundDrawable = ColorDrawable(it.getColor(R.color.bg_default))
|
||||||
val presenter = MentionAutocompletePresenter(activity, roomToken)
|
val presenter = MentionAutocompletePresenter(activity, roomToken)
|
||||||
|
@ -1691,7 +1691,7 @@ class ChatController(args: Bundle) :
|
||||||
)
|
)
|
||||||
?.subscribeOn(Schedulers.io())
|
?.subscribeOn(Schedulers.io())
|
||||||
?.observeOn(AndroidSchedulers.mainThread())
|
?.observeOn(AndroidSchedulers.mainThread())
|
||||||
?.retry(3)
|
?.retry(RETRIES)
|
||||||
?.subscribe(object : Observer<RoomOverall> {
|
?.subscribe(object : Observer<RoomOverall> {
|
||||||
override fun onSubscribe(d: Disposable) {
|
override fun onSubscribe(d: Disposable) {
|
||||||
disposableList.add(d)
|
disposableList.add(d)
|
||||||
|
@ -1951,7 +1951,7 @@ class ChatController(args: Bundle) :
|
||||||
}
|
}
|
||||||
|
|
||||||
val timeout = if (lookingIntoFuture) {
|
val timeout = if (lookingIntoFuture) {
|
||||||
30
|
LOOKING_INTO_FUTURE_TIMEOUT
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
@ -1959,7 +1959,7 @@ class ChatController(args: Bundle) :
|
||||||
fieldMap["timeout"] = timeout
|
fieldMap["timeout"] = timeout
|
||||||
|
|
||||||
fieldMap["lookIntoFuture"] = lookIntoFuture
|
fieldMap["lookIntoFuture"] = lookIntoFuture
|
||||||
fieldMap["limit"] = 100
|
fieldMap["limit"] = MESSAGE_PULL_LIMIT
|
||||||
fieldMap["setReadMarker"] = setReadMarker
|
fieldMap["setReadMarker"] = setReadMarker
|
||||||
|
|
||||||
val lastKnown: Int
|
val lastKnown: Int
|
||||||
|
@ -1999,10 +1999,10 @@ class ChatController(args: Bundle) :
|
||||||
Log.d(TAG, "pullChatMessages - pullChatMessages[lookIntoFuture > 0] - got response")
|
Log.d(TAG, "pullChatMessages - pullChatMessages[lookIntoFuture > 0] - got response")
|
||||||
pullChatMessagesPending = false
|
pullChatMessagesPending = false
|
||||||
try {
|
try {
|
||||||
if (response.code() == 304) {
|
if (response.code() == HTTP_CODE_NOT_MODIFIED) {
|
||||||
Log.d(TAG, "pullChatMessages - quasi recursive call to pullChatMessages")
|
Log.d(TAG, "pullChatMessages - quasi recursive call to pullChatMessages")
|
||||||
pullChatMessages(1, setReadMarker, xChatLastCommonRead)
|
pullChatMessages(1, setReadMarker, xChatLastCommonRead)
|
||||||
} else if (response.code() == 412) {
|
} else if (response.code() == HTTP_CODE_PRECONDITION_FAILED) {
|
||||||
futurePreconditionFailed = true
|
futurePreconditionFailed = true
|
||||||
} else {
|
} else {
|
||||||
processMessages(response, true, finalTimeout)
|
processMessages(response, true, finalTimeout)
|
||||||
|
@ -2041,7 +2041,7 @@ class ChatController(args: Bundle) :
|
||||||
Log.d(TAG, "pullChatMessages - pullChatMessages[lookIntoFuture <= 0] - got response")
|
Log.d(TAG, "pullChatMessages - pullChatMessages[lookIntoFuture <= 0] - got response")
|
||||||
pullChatMessagesPending = false
|
pullChatMessagesPending = false
|
||||||
try {
|
try {
|
||||||
if (response.code() == 412) {
|
if (response.code() == HTTP_CODE_PRECONDITION_FAILED) {
|
||||||
pastPreconditionFailed = true
|
pastPreconditionFailed = true
|
||||||
} else {
|
} else {
|
||||||
processMessages(response, false, 0)
|
processMessages(response, false, 0)
|
||||||
|
@ -2129,7 +2129,7 @@ class ChatController(args: Bundle) :
|
||||||
if (chatMessageList.size > i + 1) {
|
if (chatMessageList.size > i + 1) {
|
||||||
if (isSameDayNonSystemMessages(chatMessageList[i], chatMessageList[i + 1]) &&
|
if (isSameDayNonSystemMessages(chatMessageList[i], chatMessageList[i + 1]) &&
|
||||||
chatMessageList[i + 1].actorId == chatMessageList[i].actorId &&
|
chatMessageList[i + 1].actorId == chatMessageList[i].actorId &&
|
||||||
countGroupedMessages < 4
|
countGroupedMessages < GROUPED_MESSAGES_THRESHOLD
|
||||||
) {
|
) {
|
||||||
chatMessageList[i].isGrouped = true
|
chatMessageList[i].isGrouped = true
|
||||||
countGroupedMessages++
|
countGroupedMessages++
|
||||||
|
@ -2210,7 +2210,8 @@ class ChatController(args: Bundle) :
|
||||||
adapter!!.isPreviousSameAuthor(
|
adapter!!.isPreviousSameAuthor(
|
||||||
chatMessage.actorId,
|
chatMessage.actorId,
|
||||||
-1
|
-1
|
||||||
) && adapter!!.getSameAuthorLastMessagesCount(chatMessage.actorId) % 5 > 0
|
) && adapter!!.getSameAuthorLastMessagesCount(chatMessage.actorId) %
|
||||||
|
GROUPED_MESSAGES_SAME_AUTHOR_THRESHOLD > 0
|
||||||
)
|
)
|
||||||
chatMessage.isOneToOneConversation =
|
chatMessage.isOneToOneConversation =
|
||||||
(currentConversation?.type == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL)
|
(currentConversation?.type == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL)
|
||||||
|
@ -2245,7 +2246,7 @@ class ChatController(args: Bundle) :
|
||||||
if (inConversation) {
|
if (inConversation) {
|
||||||
pullChatMessages(1, 1, xChatLastCommonRead)
|
pullChatMessages(1, 1, xChatLastCommonRead)
|
||||||
}
|
}
|
||||||
} else if (response.code() == 304 && !isFromTheFuture) {
|
} else if (response.code() == HTTP_CODE_NOT_MODIFIED && !isFromTheFuture) {
|
||||||
if (isFirstMessagesProcessing) {
|
if (isFirstMessagesProcessing) {
|
||||||
cancelNotificationsForCurrentConversation()
|
cancelNotificationsForCurrentConversation()
|
||||||
|
|
||||||
|
@ -2478,7 +2479,7 @@ class ChatController(args: Bundle) :
|
||||||
conversationUser?.baseUrl,
|
conversationUser?.baseUrl,
|
||||||
"1",
|
"1",
|
||||||
null,
|
null,
|
||||||
message?.user?.id?.substring(6),
|
message?.user?.id?.substring(INVITE_LENGTH),
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
ncApi!!.createRoom(
|
ncApi!!.createRoom(
|
||||||
|
@ -2600,7 +2601,7 @@ class ChatController(args: Bundle) :
|
||||||
menu.findItem(R.id.action_reply_privately).isVisible = message.replyable &&
|
menu.findItem(R.id.action_reply_privately).isVisible = message.replyable &&
|
||||||
conversationUser?.userId?.isNotEmpty() == true && conversationUser.userId != "?" &&
|
conversationUser?.userId?.isNotEmpty() == true && conversationUser.userId != "?" &&
|
||||||
message.user.id.startsWith("users/") &&
|
message.user.id.startsWith("users/") &&
|
||||||
message.user.id.substring(6) != currentConversation?.actorId &&
|
message.user.id.substring(ACTOR_LENGTH) != currentConversation?.actorId &&
|
||||||
currentConversation?.type != Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL
|
currentConversation?.type != Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL
|
||||||
menu.findItem(R.id.action_delete_message).isVisible = isShowMessageDeletionButton(message)
|
menu.findItem(R.id.action_delete_message).isVisible = isShowMessageDeletionButton(message)
|
||||||
menu.findItem(R.id.action_forward_message).isVisible =
|
menu.findItem(R.id.action_forward_message).isVisible =
|
||||||
|
@ -2642,7 +2643,7 @@ class ChatController(args: Bundle) :
|
||||||
|
|
||||||
val px = TypedValue.applyDimension(
|
val px = TypedValue.applyDimension(
|
||||||
TypedValue.COMPLEX_UNIT_DIP,
|
TypedValue.COMPLEX_UNIT_DIP,
|
||||||
96f,
|
QUOTED_MESSAGE_IMAGE_MAX_HEIGHT,
|
||||||
resources?.displayMetrics
|
resources?.displayMetrics
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -2860,5 +2861,18 @@ class ChatController(args: Bundle) :
|
||||||
private const val VOICE_MESSAGE_SEEKBAR_BASE: Int = 1000
|
private const val VOICE_MESSAGE_SEEKBAR_BASE: Int = 1000
|
||||||
private const val SECOND: Long = 1000
|
private const val SECOND: Long = 1000
|
||||||
private const val NO_PREVIOUS_MESSAGE_ID: Int = -1
|
private const val NO_PREVIOUS_MESSAGE_ID: Int = -1
|
||||||
|
private const val GROUPED_MESSAGES_THRESHOLD = 4
|
||||||
|
private const val GROUPED_MESSAGES_SAME_AUTHOR_THRESHOLD = 5
|
||||||
|
private const val TOOLBAR_AVATAR_RATIO = 1.5
|
||||||
|
private const val HTTP_CODE_NOT_MODIFIED = 304
|
||||||
|
private const val HTTP_CODE_PRECONDITION_FAILED = 412
|
||||||
|
private const val QUOTED_MESSAGE_IMAGE_MAX_HEIGHT = 96f
|
||||||
|
private const val MENTION_AUTO_COMPLETE_ELEVATION = 6f
|
||||||
|
private const val MESSAGE_PULL_LIMIT = 100
|
||||||
|
private const val INVITE_LENGTH = 6
|
||||||
|
private const val ACTOR_LENGTH = 6
|
||||||
|
private const val ANIMATION_DURATION: Long = 750
|
||||||
|
private const val RETRIES: Long = 3
|
||||||
|
private const val LOOKING_INTO_FUTURE_TIMEOUT = 30
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,6 +72,7 @@ import com.nextcloud.talk.models.json.participants.Participant.ActorType.GROUPS
|
||||||
import com.nextcloud.talk.models.json.participants.Participant.ActorType.USERS
|
import com.nextcloud.talk.models.json.participants.Participant.ActorType.USERS
|
||||||
import com.nextcloud.talk.models.json.participants.ParticipantsOverall
|
import com.nextcloud.talk.models.json.participants.ParticipantsOverall
|
||||||
import com.nextcloud.talk.utils.ApiUtils
|
import com.nextcloud.talk.utils.ApiUtils
|
||||||
|
import com.nextcloud.talk.utils.DateConstants
|
||||||
import com.nextcloud.talk.utils.DateUtils
|
import com.nextcloud.talk.utils.DateUtils
|
||||||
import com.nextcloud.talk.utils.DisplayUtils
|
import com.nextcloud.talk.utils.DisplayUtils
|
||||||
import com.nextcloud.talk.utils.bundle.BundleKeys
|
import com.nextcloud.talk.utils.bundle.BundleKeys
|
||||||
|
@ -206,7 +207,7 @@ class ConversationInfoController(args: Bundle) :
|
||||||
MaterialDialog(activity!!, BottomSheet(WRAP_CONTENT)).show {
|
MaterialDialog(activity!!, BottomSheet(WRAP_CONTENT)).show {
|
||||||
val currentTimeCalendar = Calendar.getInstance()
|
val currentTimeCalendar = Calendar.getInstance()
|
||||||
if (conversation!!.lobbyTimer != null && conversation!!.lobbyTimer != 0L) {
|
if (conversation!!.lobbyTimer != null && conversation!!.lobbyTimer != 0L) {
|
||||||
currentTimeCalendar.timeInMillis = conversation!!.lobbyTimer * 1000
|
currentTimeCalendar.timeInMillis = conversation!!.lobbyTimer * DateConstants.SECOND_DIVIDER
|
||||||
}
|
}
|
||||||
|
|
||||||
dateTimePicker(
|
dateTimePicker(
|
||||||
|
@ -238,13 +239,15 @@ class ConversationInfoController(args: Bundle) :
|
||||||
conversation.type == Conversation.ConversationType.ROOM_PUBLIC_CALL
|
conversation.type == Conversation.ConversationType.ROOM_PUBLIC_CALL
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reconfigureLobbyTimerView(dateTime: Calendar? = null) {
|
private fun reconfigureLobbyTimerView(dateTime: Calendar? = null) {
|
||||||
val isChecked =
|
val isChecked =
|
||||||
(binding.webinarInfoView.conversationInfoLobby.findViewById<View>(R.id.mp_checkable) as SwitchCompat)
|
(binding.webinarInfoView.conversationInfoLobby.findViewById<View>(R.id.mp_checkable) as SwitchCompat)
|
||||||
.isChecked
|
.isChecked
|
||||||
|
|
||||||
if (dateTime != null && isChecked) {
|
if (dateTime != null && isChecked) {
|
||||||
conversation!!.lobbyTimer = (dateTime.timeInMillis - (dateTime.time.seconds * 1000)) / 1000
|
conversation!!.lobbyTimer = (
|
||||||
|
dateTime.timeInMillis - (dateTime.time.seconds * DateConstants.SECOND_DIVIDER)
|
||||||
|
) / DateConstants.SECOND_DIVIDER
|
||||||
} else if (!isChecked) {
|
} else if (!isChecked) {
|
||||||
conversation!!.lobbyTimer = 0
|
conversation!!.lobbyTimer = 0
|
||||||
}
|
}
|
||||||
|
@ -683,9 +686,9 @@ class ConversationInfoController(args: Bundle) :
|
||||||
if (conversation!!.notificationLevel != Conversation.NotificationLevel.DEFAULT) {
|
if (conversation!!.notificationLevel != Conversation.NotificationLevel.DEFAULT) {
|
||||||
val stringValue: String =
|
val stringValue: String =
|
||||||
when (EnumNotificationLevelConverter().convertToInt(conversation!!.notificationLevel)) {
|
when (EnumNotificationLevelConverter().convertToInt(conversation!!.notificationLevel)) {
|
||||||
1 -> "always"
|
NOTIFICATION_LEVEL_ALWAYS -> "always"
|
||||||
2 -> "mention"
|
NOTIFICATION_LEVEL_MENTION -> "mention"
|
||||||
3 -> "never"
|
NOTIFICATION_LEVEL_NEVER -> "never"
|
||||||
else -> "mention"
|
else -> "mention"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1100,7 +1103,10 @@ class ConversationInfoController(args: Bundle) :
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "ConversationInfControll"
|
private const val TAG = "ConversationInfo"
|
||||||
|
private const val NOTIFICATION_LEVEL_ALWAYS: Int = 1
|
||||||
|
private const val NOTIFICATION_LEVEL_MENTION: Int = 2
|
||||||
|
private const val NOTIFICATION_LEVEL_NEVER: Int = 3
|
||||||
private const val ID_DELETE_CONVERSATION_DIALOG = 0
|
private const val ID_DELETE_CONVERSATION_DIALOG = 0
|
||||||
private const val ID_CLEAR_CHAT_DIALOG = 1
|
private const val ID_CLEAR_CHAT_DIALOG = 1
|
||||||
private val LOW_EMPHASIS_OPACITY: Float = 0.38f
|
private val LOW_EMPHASIS_OPACITY: Float = 0.38f
|
||||||
|
|
|
@ -1,300 +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.controllers;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.media.MediaPlayer;
|
|
||||||
import android.media.RingtoneManager;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
|
||||||
import autodagger.AutoInjector;
|
|
||||||
import butterknife.BindView;
|
|
||||||
import com.bluelinelabs.logansquare.LoganSquare;
|
|
||||||
import com.nextcloud.talk.R;
|
|
||||||
import com.nextcloud.talk.adapters.items.NotificationSoundItem;
|
|
||||||
import com.nextcloud.talk.application.NextcloudTalkApplication;
|
|
||||||
import com.nextcloud.talk.controllers.base.BaseController;
|
|
||||||
import com.nextcloud.talk.models.RingtoneSettings;
|
|
||||||
import com.nextcloud.talk.utils.NotificationUtils;
|
|
||||||
import com.nextcloud.talk.utils.bundle.BundleKeys;
|
|
||||||
import com.nextcloud.talk.utils.preferences.AppPreferences;
|
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter;
|
|
||||||
import eu.davidea.flexibleadapter.SelectableAdapter;
|
|
||||||
import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager;
|
|
||||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@AutoInjector(NextcloudTalkApplication.class)
|
|
||||||
public class RingtoneSelectionController extends BaseController implements FlexibleAdapter.OnItemClickListener {
|
|
||||||
|
|
||||||
private static final String TAG = "RingtoneSelectionController";
|
|
||||||
|
|
||||||
@BindView(R.id.recycler_view)
|
|
||||||
RecyclerView recyclerView;
|
|
||||||
|
|
||||||
@BindView(R.id.swipe_refresh_layout)
|
|
||||||
SwipeRefreshLayout swipeRefreshLayout;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
AppPreferences appPreferences;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
Context context;
|
|
||||||
|
|
||||||
private FlexibleAdapter adapter;
|
|
||||||
private RecyclerView.AdapterDataObserver adapterDataObserver;
|
|
||||||
private List<AbstractFlexibleItem> abstractFlexibleItemList = new ArrayList<>();
|
|
||||||
|
|
||||||
private boolean callNotificationSounds;
|
|
||||||
private MediaPlayer mediaPlayer;
|
|
||||||
private Handler cancelMediaPlayerHandler;
|
|
||||||
|
|
||||||
public RingtoneSelectionController(Bundle args) {
|
|
||||||
super(args);
|
|
||||||
setHasOptionsMenu(true);
|
|
||||||
this.callNotificationSounds = args.getBoolean(BundleKeys.INSTANCE.getKEY_ARE_CALL_SOUNDS(), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) {
|
|
||||||
return inflater.inflate(R.layout.controller_generic_rv, container, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onViewBound(@NonNull View view) {
|
|
||||||
super.onViewBound(view);
|
|
||||||
NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this);
|
|
||||||
|
|
||||||
if (adapter == null) {
|
|
||||||
adapter = new FlexibleAdapter<>(abstractFlexibleItemList, getActivity(), false);
|
|
||||||
|
|
||||||
adapter.setNotifyChangeOfUnfilteredItems(true)
|
|
||||||
.setMode(SelectableAdapter.Mode.SINGLE);
|
|
||||||
|
|
||||||
adapter.addListener(this);
|
|
||||||
|
|
||||||
cancelMediaPlayerHandler = new Handler();
|
|
||||||
}
|
|
||||||
|
|
||||||
adapter.addListener(this);
|
|
||||||
prepareViews();
|
|
||||||
fetchNotificationSounds();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
|
||||||
if (item.getItemId() == android.R.id.home) {
|
|
||||||
return getRouter().popCurrentController();
|
|
||||||
}
|
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void prepareViews() {
|
|
||||||
RecyclerView.LayoutManager layoutManager = new SmoothScrollLinearLayoutManager(getActivity());
|
|
||||||
recyclerView.setLayoutManager(layoutManager);
|
|
||||||
recyclerView.setHasFixedSize(true);
|
|
||||||
recyclerView.setAdapter(adapter);
|
|
||||||
|
|
||||||
adapterDataObserver = new RecyclerView.AdapterDataObserver() {
|
|
||||||
@Override
|
|
||||||
public void onChanged() {
|
|
||||||
super.onChanged();
|
|
||||||
findSelectedSound();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
adapter.registerAdapterDataObserver(adapterDataObserver);
|
|
||||||
swipeRefreshLayout.setEnabled(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("LongLogTag")
|
|
||||||
private void findSelectedSound() {
|
|
||||||
boolean foundDefault = false;
|
|
||||||
|
|
||||||
String preferencesString = null;
|
|
||||||
if ((callNotificationSounds && TextUtils.isEmpty((preferencesString = appPreferences.getCallRingtoneUri())))
|
|
||||||
|| (!callNotificationSounds && TextUtils.isEmpty((preferencesString = appPreferences
|
|
||||||
.getMessageRingtoneUri())))) {
|
|
||||||
adapter.toggleSelection(1);
|
|
||||||
foundDefault = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TextUtils.isEmpty(preferencesString) && !foundDefault) {
|
|
||||||
try {
|
|
||||||
RingtoneSettings ringtoneSettings = LoganSquare.parse(preferencesString, RingtoneSettings.class);
|
|
||||||
if (ringtoneSettings.getRingtoneUri() == null) {
|
|
||||||
adapter.toggleSelection(0);
|
|
||||||
} else if (ringtoneSettings.getRingtoneUri().toString().equals(getRingtoneString())) {
|
|
||||||
adapter.toggleSelection(1);
|
|
||||||
} else {
|
|
||||||
NotificationSoundItem notificationSoundItem;
|
|
||||||
for (int i = 2; i < adapter.getItemCount(); i++) {
|
|
||||||
notificationSoundItem = (NotificationSoundItem) adapter.getItem(i);
|
|
||||||
if (notificationSoundItem.getNotificationSoundUri().equals(ringtoneSettings.getRingtoneUri().toString())) {
|
|
||||||
adapter.toggleSelection(i);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.e(TAG, "Failed to parse ringtone settings");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
adapter.unregisterAdapterDataObserver(adapterDataObserver);
|
|
||||||
adapterDataObserver = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getRingtoneString() {
|
|
||||||
if (callNotificationSounds) {
|
|
||||||
return NotificationUtils.DEFAULT_CALL_RINGTONE_URI;
|
|
||||||
} else {
|
|
||||||
return NotificationUtils.DEFAULT_MESSAGE_RINGTONE_URI;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void fetchNotificationSounds() {
|
|
||||||
abstractFlexibleItemList.add(new NotificationSoundItem(getResources().getString(R.string.nc_settings_no_ringtone), null));
|
|
||||||
abstractFlexibleItemList.add(new NotificationSoundItem(getResources()
|
|
||||||
.getString(R.string.nc_settings_default_ringtone), getRingtoneString()));
|
|
||||||
|
|
||||||
|
|
||||||
if (getActivity() != null) {
|
|
||||||
RingtoneManager manager = new RingtoneManager(getActivity());
|
|
||||||
|
|
||||||
if (callNotificationSounds) {
|
|
||||||
manager.setType(RingtoneManager.TYPE_RINGTONE);
|
|
||||||
} else {
|
|
||||||
manager.setType(RingtoneManager.TYPE_NOTIFICATION);
|
|
||||||
}
|
|
||||||
|
|
||||||
Cursor cursor = manager.getCursor();
|
|
||||||
|
|
||||||
NotificationSoundItem notificationSoundItem;
|
|
||||||
|
|
||||||
while (cursor.moveToNext()) {
|
|
||||||
String notificationTitle = cursor.getString(RingtoneManager.TITLE_COLUMN_INDEX);
|
|
||||||
String notificationUri = cursor.getString(RingtoneManager.URI_COLUMN_INDEX);
|
|
||||||
String completeNotificationUri = notificationUri + "/" + cursor.getString(RingtoneManager
|
|
||||||
.ID_COLUMN_INDEX);
|
|
||||||
|
|
||||||
notificationSoundItem = new NotificationSoundItem(notificationTitle, completeNotificationUri);
|
|
||||||
|
|
||||||
abstractFlexibleItemList.add(notificationSoundItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
adapter.updateDataSet(abstractFlexibleItemList, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String getTitle() {
|
|
||||||
return getResources().getString(R.string.nc_settings_notification_sounds);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("LongLogTag")
|
|
||||||
@Override
|
|
||||||
public boolean onItemClick(View view, int position) {
|
|
||||||
NotificationSoundItem notificationSoundItem = (NotificationSoundItem) adapter.getItem(position);
|
|
||||||
|
|
||||||
Uri ringtoneUri = null;
|
|
||||||
|
|
||||||
if (!TextUtils.isEmpty(notificationSoundItem.getNotificationSoundUri())) {
|
|
||||||
ringtoneUri = Uri.parse(notificationSoundItem.getNotificationSoundUri());
|
|
||||||
|
|
||||||
endMediaPlayer();
|
|
||||||
mediaPlayer = MediaPlayer.create(getActivity(), ringtoneUri);
|
|
||||||
|
|
||||||
cancelMediaPlayerHandler = new Handler();
|
|
||||||
cancelMediaPlayerHandler.postDelayed(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
endMediaPlayer();
|
|
||||||
}
|
|
||||||
}, mediaPlayer.getDuration() + 25);
|
|
||||||
mediaPlayer.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (adapter.getSelectedPositions().size() == 0 || adapter.getSelectedPositions().get(0) != position) {
|
|
||||||
RingtoneSettings ringtoneSettings = new RingtoneSettings();
|
|
||||||
ringtoneSettings.setRingtoneName(notificationSoundItem.getNotificationSoundName());
|
|
||||||
ringtoneSettings.setRingtoneUri(ringtoneUri);
|
|
||||||
|
|
||||||
if (callNotificationSounds) {
|
|
||||||
try {
|
|
||||||
appPreferences.setCallRingtoneUri(LoganSquare.serialize(ringtoneSettings));
|
|
||||||
adapter.toggleSelection(position);
|
|
||||||
adapter.notifyDataSetChanged();
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.e(TAG, "Failed to store selected ringtone for calls");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
appPreferences.setMessageRingtoneUri(LoganSquare.serialize(ringtoneSettings));
|
|
||||||
adapter.toggleSelection(position);
|
|
||||||
adapter.notifyDataSetChanged();
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.e(TAG, "Failed to store selected ringtone for calls");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void endMediaPlayer() {
|
|
||||||
if (cancelMediaPlayerHandler != null) {
|
|
||||||
cancelMediaPlayerHandler.removeCallbacksAndMessages(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mediaPlayer != null) {
|
|
||||||
if (mediaPlayer.isPlaying()) {
|
|
||||||
mediaPlayer.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
mediaPlayer.release();
|
|
||||||
mediaPlayer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
endMediaPlayer();
|
|
||||||
super.onDestroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,257 @@
|
||||||
|
/*
|
||||||
|
* Nextcloud Talk application
|
||||||
|
*
|
||||||
|
* @author Mario Danic
|
||||||
|
* @author Andy Scherzinger
|
||||||
|
* Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
|
||||||
|
* 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.controllers
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.media.MediaPlayer
|
||||||
|
import android.media.RingtoneManager
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.text.TextUtils
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import autodagger.AutoInjector
|
||||||
|
import com.bluelinelabs.logansquare.LoganSquare
|
||||||
|
import com.nextcloud.talk.R
|
||||||
|
import com.nextcloud.talk.adapters.items.NotificationSoundItem
|
||||||
|
import com.nextcloud.talk.application.NextcloudTalkApplication
|
||||||
|
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
|
||||||
|
import com.nextcloud.talk.controllers.base.NewBaseController
|
||||||
|
import com.nextcloud.talk.controllers.util.viewBinding
|
||||||
|
import com.nextcloud.talk.databinding.ControllerGenericRvBinding
|
||||||
|
import com.nextcloud.talk.models.RingtoneSettings
|
||||||
|
import com.nextcloud.talk.utils.NotificationUtils
|
||||||
|
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ARE_CALL_SOUNDS
|
||||||
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
|
import eu.davidea.flexibleadapter.SelectableAdapter
|
||||||
|
import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager
|
||||||
|
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.ArrayList
|
||||||
|
|
||||||
|
@AutoInjector(NextcloudTalkApplication::class)
|
||||||
|
class RingtoneSelectionController(args: Bundle) :
|
||||||
|
NewBaseController(
|
||||||
|
R.layout.controller_generic_rv,
|
||||||
|
args
|
||||||
|
),
|
||||||
|
FlexibleAdapter.OnItemClickListener {
|
||||||
|
private val binding: ControllerGenericRvBinding by viewBinding(ControllerGenericRvBinding::bind)
|
||||||
|
|
||||||
|
private var adapter: FlexibleAdapter<*>? = null
|
||||||
|
private var adapterDataObserver: RecyclerView.AdapterDataObserver? = null
|
||||||
|
private val abstractFlexibleItemList: MutableList<AbstractFlexibleItem<*>> = ArrayList()
|
||||||
|
private val callNotificationSounds: Boolean
|
||||||
|
private var mediaPlayer: MediaPlayer? = null
|
||||||
|
private var cancelMediaPlayerHandler: Handler? = null
|
||||||
|
|
||||||
|
override fun onViewBound(view: View) {
|
||||||
|
super.onViewBound(view)
|
||||||
|
if (adapter == null) {
|
||||||
|
adapter = FlexibleAdapter(abstractFlexibleItemList, activity, false)
|
||||||
|
adapter!!.setNotifyChangeOfUnfilteredItems(true).mode = SelectableAdapter.Mode.SINGLE
|
||||||
|
adapter!!.addListener(this)
|
||||||
|
cancelMediaPlayerHandler = Handler()
|
||||||
|
}
|
||||||
|
adapter!!.addListener(this)
|
||||||
|
prepareViews()
|
||||||
|
fetchNotificationSounds()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
return if (item.itemId == android.R.id.home) {
|
||||||
|
router.popCurrentController()
|
||||||
|
} else {
|
||||||
|
super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun prepareViews() {
|
||||||
|
val layoutManager: RecyclerView.LayoutManager = SmoothScrollLinearLayoutManager(activity)
|
||||||
|
binding.recyclerView.layoutManager = layoutManager
|
||||||
|
binding.recyclerView.setHasFixedSize(true)
|
||||||
|
binding.recyclerView.adapter = adapter
|
||||||
|
adapterDataObserver = object : RecyclerView.AdapterDataObserver() {
|
||||||
|
override fun onChanged() {
|
||||||
|
super.onChanged()
|
||||||
|
findSelectedSound()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
adapter!!.registerAdapterDataObserver(adapterDataObserver!!)
|
||||||
|
binding.swipeRefreshLayout.isEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("LongLogTag")
|
||||||
|
private fun findSelectedSound() {
|
||||||
|
var foundDefault = false
|
||||||
|
var preferencesString: String? = null
|
||||||
|
if (callNotificationSounds &&
|
||||||
|
TextUtils.isEmpty(appPreferences!!.callRingtoneUri.also { preferencesString = it }) ||
|
||||||
|
!callNotificationSounds &&
|
||||||
|
TextUtils.isEmpty(appPreferences!!.messageRingtoneUri.also { preferencesString = it })
|
||||||
|
) {
|
||||||
|
adapter!!.toggleSelection(1)
|
||||||
|
foundDefault = true
|
||||||
|
}
|
||||||
|
if (!TextUtils.isEmpty(preferencesString) && !foundDefault) {
|
||||||
|
try {
|
||||||
|
val ringtoneSettings: RingtoneSettings =
|
||||||
|
LoganSquare.parse<RingtoneSettings>(preferencesString, RingtoneSettings::class.java)
|
||||||
|
if (ringtoneSettings.getRingtoneUri() == null) {
|
||||||
|
adapter!!.toggleSelection(0)
|
||||||
|
} else if (ringtoneSettings.getRingtoneUri().toString() == ringtoneString) {
|
||||||
|
adapter!!.toggleSelection(1)
|
||||||
|
} else {
|
||||||
|
var notificationSoundItem: NotificationSoundItem?
|
||||||
|
for (i in 2 until adapter!!.itemCount) {
|
||||||
|
notificationSoundItem = adapter!!.getItem(i) as NotificationSoundItem?
|
||||||
|
if (
|
||||||
|
notificationSoundItem!!.notificationSoundUri == ringtoneSettings.getRingtoneUri().toString()
|
||||||
|
) {
|
||||||
|
adapter!!.toggleSelection(i)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.e(TAG, "Failed to parse ringtone settings")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
adapter!!.unregisterAdapterDataObserver(adapterDataObserver!!)
|
||||||
|
adapterDataObserver = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private val ringtoneString: String
|
||||||
|
get() = if (callNotificationSounds) {
|
||||||
|
NotificationUtils.DEFAULT_CALL_RINGTONE_URI
|
||||||
|
} else {
|
||||||
|
NotificationUtils.DEFAULT_MESSAGE_RINGTONE_URI
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fetchNotificationSounds() {
|
||||||
|
abstractFlexibleItemList.add(
|
||||||
|
NotificationSoundItem(
|
||||||
|
resources!!.getString(R.string.nc_settings_no_ringtone),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
abstractFlexibleItemList.add(
|
||||||
|
NotificationSoundItem(
|
||||||
|
resources!!.getString(R.string.nc_settings_default_ringtone),
|
||||||
|
ringtoneString
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (activity != null) {
|
||||||
|
val manager = RingtoneManager(activity)
|
||||||
|
if (callNotificationSounds) {
|
||||||
|
manager.setType(RingtoneManager.TYPE_RINGTONE)
|
||||||
|
} else {
|
||||||
|
manager.setType(RingtoneManager.TYPE_NOTIFICATION)
|
||||||
|
}
|
||||||
|
val cursor = manager.cursor
|
||||||
|
var notificationSoundItem: NotificationSoundItem
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
val notificationTitle = cursor.getString(RingtoneManager.TITLE_COLUMN_INDEX)
|
||||||
|
val notificationUri = cursor.getString(RingtoneManager.URI_COLUMN_INDEX)
|
||||||
|
val completeNotificationUri = notificationUri + "/" + cursor.getString(RingtoneManager.ID_COLUMN_INDEX)
|
||||||
|
notificationSoundItem = NotificationSoundItem(notificationTitle, completeNotificationUri)
|
||||||
|
abstractFlexibleItemList.add(notificationSoundItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
adapter!!.updateDataSet(abstractFlexibleItemList as List<Nothing>?, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemClick(view: View, position: Int): Boolean {
|
||||||
|
val notificationSoundItem = adapter!!.getItem(position) as NotificationSoundItem?
|
||||||
|
var ringtoneUri: Uri? = null
|
||||||
|
if (!TextUtils.isEmpty(notificationSoundItem!!.notificationSoundUri)) {
|
||||||
|
ringtoneUri = Uri.parse(notificationSoundItem.notificationSoundUri)
|
||||||
|
endMediaPlayer()
|
||||||
|
mediaPlayer = MediaPlayer.create(activity, ringtoneUri)
|
||||||
|
cancelMediaPlayerHandler = Handler()
|
||||||
|
cancelMediaPlayerHandler!!.postDelayed(
|
||||||
|
{ endMediaPlayer() },
|
||||||
|
(mediaPlayer!!.duration + DURATION_EXTENSION).toLong()
|
||||||
|
)
|
||||||
|
mediaPlayer!!.start()
|
||||||
|
}
|
||||||
|
if (adapter!!.selectedPositions.size == 0 || adapter!!.selectedPositions[0] != position) {
|
||||||
|
val ringtoneSettings = RingtoneSettings()
|
||||||
|
ringtoneSettings.setRingtoneName(notificationSoundItem.notificationSoundName)
|
||||||
|
ringtoneSettings.setRingtoneUri(ringtoneUri)
|
||||||
|
if (callNotificationSounds) {
|
||||||
|
try {
|
||||||
|
appPreferences!!.callRingtoneUri = LoganSquare.serialize(ringtoneSettings)
|
||||||
|
adapter!!.toggleSelection(position)
|
||||||
|
adapter!!.notifyDataSetChanged()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.e(TAG, "Failed to store selected ringtone for calls")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
appPreferences!!.messageRingtoneUri = LoganSquare.serialize(ringtoneSettings)
|
||||||
|
adapter!!.toggleSelection(position)
|
||||||
|
adapter!!.notifyDataSetChanged()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.e(TAG, "Failed to store selected ringtone for calls")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun endMediaPlayer() {
|
||||||
|
if (cancelMediaPlayerHandler != null) {
|
||||||
|
cancelMediaPlayerHandler!!.removeCallbacksAndMessages(null)
|
||||||
|
}
|
||||||
|
if (mediaPlayer != null) {
|
||||||
|
if (mediaPlayer!!.isPlaying) {
|
||||||
|
mediaPlayer!!.stop()
|
||||||
|
}
|
||||||
|
mediaPlayer!!.release()
|
||||||
|
mediaPlayer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override fun onDestroy() {
|
||||||
|
endMediaPlayer()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "RingtoneSelection"
|
||||||
|
private const val DURATION_EXTENSION = 25
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
setHasOptionsMenu(true)
|
||||||
|
sharedApplication!!.componentApplication.inject(this)
|
||||||
|
callNotificationSounds = args.getBoolean(KEY_ARE_CALL_SOUNDS, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val title: String
|
||||||
|
get() =
|
||||||
|
resources!!.getString(R.string.nc_settings_notification_sounds)
|
||||||
|
}
|
|
@ -213,7 +213,7 @@ class ServerSelectionController : NewBaseController(R.layout.controller_server_s
|
||||||
val productName = resources!!.getString(R.string.nc_server_product_name)
|
val productName = resources!!.getString(R.string.nc_server_product_name)
|
||||||
val versionString: String = status.getVersion().substring(0, status.getVersion().indexOf("."))
|
val versionString: String = status.getVersion().substring(0, status.getVersion().indexOf("."))
|
||||||
val version: Int = versionString.toInt()
|
val version: Int = versionString.toInt()
|
||||||
if (isServerStatusQueryable(status) && version >= 13) {
|
if (isServerStatusQueryable(status) && version >= MIN_SERVER_MAJOR_VERSION) {
|
||||||
router.pushController(
|
router.pushController(
|
||||||
RouterTransaction.with(
|
RouterTransaction.with(
|
||||||
WebViewLoginController(
|
WebViewLoginController(
|
||||||
|
@ -369,5 +369,6 @@ class ServerSelectionController : NewBaseController(R.layout.controller_server_s
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = "ServerSelectionController"
|
const val TAG = "ServerSelectionController"
|
||||||
|
const val MIN_SERVER_MAJOR_VERSION = 13
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
*
|
*
|
||||||
* @author Mario Danic
|
* @author Mario Danic
|
||||||
* @author Andy Scherzinger
|
* @author Andy Scherzinger
|
||||||
* Copyright (C) 2022 Andy Scherzinger (info@andy-scherzinger.de)
|
* Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
|
||||||
* Copyright (C) 2017 Mario Danic <mario@lovelyhq.com>
|
* Copyright (C) 2017 Mario Danic <mario@lovelyhq.com>
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
|
|
@ -1,504 +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 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.DisplayUtils;
|
|
||||||
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 org.greenrobot.eventbus.EventBus;
|
|
||||||
|
|
||||||
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 androidx.annotation.NonNull;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import androidx.core.content.res.ResourcesCompat;
|
|
||||||
import androidx.work.Data;
|
|
||||||
import androidx.work.OneTimeWorkRequest;
|
|
||||||
import androidx.work.WorkManager;
|
|
||||||
import autodagger.AutoInjector;
|
|
||||||
import butterknife.BindView;
|
|
||||||
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;
|
|
||||||
|
|
||||||
@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(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;
|
|
||||||
}
|
|
||||||
|
|
||||||
public WebViewLoginController(Bundle args) {
|
|
||||||
super(args);
|
|
||||||
}
|
|
||||||
|
|
||||||
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_product_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, "", Boolean.TRUE,
|
|
||||||
null, currentUser.getId(), null, appPreferences.getTemporaryClientCertAlias(), null)
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(userEntity -> {
|
|
||||||
if (finalMessageType != null) {
|
|
||||||
ApplicationWideMessageHolder.getInstance().setMessageType(finalMessageType);
|
|
||||||
}
|
|
||||||
|
|
||||||
Data data =
|
|
||||||
new Data.Builder().putString(PushRegistrationWorker.ORIGIN,
|
|
||||||
"WebViewLoginController#parseAndLoginFromWebView").build();
|
|
||||||
OneTimeWorkRequest pushRegistrationWork = new OneTimeWorkRequest.Builder(PushRegistrationWorker.class)
|
|
||||||
.setInputData(data)
|
|
||||||
.build();
|
|
||||||
WorkManager.getInstance().enqueue(pushRegistrationWork);
|
|
||||||
|
|
||||||
getRouter().popCurrentController();
|
|
||||||
}, throwable -> dispose(),
|
|
||||||
this::dispose);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (finalMessageType != null) {
|
|
||||||
// FIXME when the user registers a new account that was setup before (aka
|
|
||||||
// ApplicationWideMessageHolder.MessageType.ACCOUNT_UPDATED_NOT_ADDED)
|
|
||||||
// The token is not updated in the database and therefor the account not visible/usable
|
|
||||||
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
|
|
||||||
protected void onAttach(@NonNull View view) {
|
|
||||||
super.onAttach(view);
|
|
||||||
|
|
||||||
if (getActivity() != null && getResources() != null) {
|
|
||||||
DisplayUtils.applyColorToStatusBar(getActivity(), ResourcesCompat.getColor(getResources(), R.color.colorPrimary, null));
|
|
||||||
DisplayUtils.applyColorToNavigationBar(getActivity().getWindow(), ResourcesCompat.getColor(getResources(), R.color.colorPrimary, 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public AppBarLayoutType getAppBarLayoutType() {
|
|
||||||
return AppBarLayoutType.EMPTY;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,473 @@
|
||||||
|
/*
|
||||||
|
* Nextcloud Talk application
|
||||||
|
*
|
||||||
|
* @author Mario Danic
|
||||||
|
* @author Andy Scherzinger
|
||||||
|
* Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
|
||||||
|
* 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.SslError
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.security.KeyChain
|
||||||
|
import android.security.KeyChainException
|
||||||
|
import android.text.TextUtils
|
||||||
|
import android.view.View
|
||||||
|
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 androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.content.res.ResourcesCompat
|
||||||
|
import androidx.work.Data
|
||||||
|
import androidx.work.OneTimeWorkRequest
|
||||||
|
import androidx.work.WorkManager
|
||||||
|
import autodagger.AutoInjector
|
||||||
|
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.application.NextcloudTalkApplication.Companion.sharedApplication
|
||||||
|
import com.nextcloud.talk.controllers.base.NewBaseController
|
||||||
|
import com.nextcloud.talk.controllers.util.viewBinding
|
||||||
|
import com.nextcloud.talk.databinding.ControllerWebViewLoginBinding
|
||||||
|
import com.nextcloud.talk.events.CertificateEvent
|
||||||
|
import com.nextcloud.talk.jobs.PushRegistrationWorker
|
||||||
|
import com.nextcloud.talk.models.LoginData
|
||||||
|
import com.nextcloud.talk.utils.DisplayUtils
|
||||||
|
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.database.user.UserUtils
|
||||||
|
import com.nextcloud.talk.utils.singletons.ApplicationWideMessageHolder
|
||||||
|
import com.nextcloud.talk.utils.ssl.MagicTrustManager
|
||||||
|
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 org.greenrobot.eventbus.EventBus
|
||||||
|
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 javax.inject.Inject
|
||||||
|
|
||||||
|
@AutoInjector(NextcloudTalkApplication::class)
|
||||||
|
class WebViewLoginController(args: Bundle? = null) : NewBaseController(
|
||||||
|
R.layout.controller_web_view_login,
|
||||||
|
args
|
||||||
|
) {
|
||||||
|
private val binding: ControllerWebViewLoginBinding by viewBinding(ControllerWebViewLoginBinding::bind)
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var userUtils: UserUtils
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var dataStore: ReactiveEntityStore<Persistable>
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var magicTrustManager: MagicTrustManager
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var eventBus: EventBus
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var cookieManager: CookieManager
|
||||||
|
|
||||||
|
private var assembledPrefix: String? = null
|
||||||
|
private var userQueryDisposable: Disposable? = 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(baseUrl: String?, isPasswordUpdate: Boolean) : this() {
|
||||||
|
this.baseUrl = baseUrl
|
||||||
|
this.isPasswordUpdate = isPasswordUpdate
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(baseUrl: String?, isPasswordUpdate: Boolean, username: String?, password: String?) : this() {
|
||||||
|
this.baseUrl = baseUrl
|
||||||
|
this.isPasswordUpdate = isPasswordUpdate
|
||||||
|
this.username = username
|
||||||
|
this.password = password
|
||||||
|
}
|
||||||
|
|
||||||
|
private val webLoginUserAgent: String
|
||||||
|
get() = (
|
||||||
|
Build.MANUFACTURER.substring(0, 1).toUpperCase(Locale.getDefault()) +
|
||||||
|
Build.MANUFACTURER.substring(1).toLowerCase(Locale.getDefault()) +
|
||||||
|
" " +
|
||||||
|
Build.MODEL +
|
||||||
|
" (" +
|
||||||
|
resources!!.getString(R.string.nc_app_product_name) +
|
||||||
|
")"
|
||||||
|
)
|
||||||
|
|
||||||
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
|
override fun onViewBound(view: View) {
|
||||||
|
super.onViewBound(view)
|
||||||
|
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||||
|
|
||||||
|
actionBar?.hide()
|
||||||
|
|
||||||
|
assembledPrefix = resources!!.getString(R.string.nc_talk_login_scheme) + PROTOCOL_SUFFIX + "login/"
|
||||||
|
binding.webview.settings.allowFileAccess = false
|
||||||
|
binding.webview.settings.allowFileAccessFromFileURLs = false
|
||||||
|
binding.webview.settings.javaScriptEnabled = true
|
||||||
|
binding.webview.settings.javaScriptCanOpenWindowsAutomatically = false
|
||||||
|
binding.webview.settings.domStorageEnabled = true
|
||||||
|
binding.webview.settings.setUserAgentString(webLoginUserAgent)
|
||||||
|
binding.webview.settings.saveFormData = false
|
||||||
|
binding.webview.settings.savePassword = false
|
||||||
|
binding.webview.settings.setRenderPriority(WebSettings.RenderPriority.HIGH)
|
||||||
|
binding.webview.clearCache(true)
|
||||||
|
binding.webview.clearFormData()
|
||||||
|
binding.webview.clearHistory()
|
||||||
|
WebView.clearClientCertPreferences(null)
|
||||||
|
webViewFidoBridge = WebViewFidoBridge.createInstanceForWebView(activity as AppCompatActivity?, binding.webview)
|
||||||
|
CookieSyncManager.createInstance(activity)
|
||||||
|
android.webkit.CookieManager.getInstance().removeAllCookies(null)
|
||||||
|
val headers: MutableMap<String, String> = HashMap()
|
||||||
|
headers.put("OCS-APIRequest", "true")
|
||||||
|
binding.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 onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
|
||||||
|
super.onPageStarted(view, url, favicon)
|
||||||
|
webViewFidoBridge?.delegateOnPageStarted(view, url, favicon)
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
binding.progressBar.visibility = View.GONE
|
||||||
|
binding.webview.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
basePageLoaded = true
|
||||||
|
}
|
||||||
|
if (!TextUtils.isEmpty(username)) {
|
||||||
|
if (loginStep == 1) {
|
||||||
|
binding.webview.loadUrl("javascript: {document.getElementsByClassName('login')[0].click(); };")
|
||||||
|
} else if (!automatedLoginAttempted) {
|
||||||
|
automatedLoginAttempted = true
|
||||||
|
if (TextUtils.isEmpty(password)) {
|
||||||
|
binding.webview.loadUrl(
|
||||||
|
"javascript:var justStore = document.getElementById('user').value = '$username';"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
binding.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 = userUtils.currentUser
|
||||||
|
var alias: String? = null
|
||||||
|
if (!isPasswordUpdate) {
|
||||||
|
alias = appPreferences!!.temporaryClientCertAlias
|
||||||
|
}
|
||||||
|
if (TextUtils.isEmpty(alias) && userEntity != null) {
|
||||||
|
alias = userEntity.clientCertificate
|
||||||
|
}
|
||||||
|
if (!TextUtils.isEmpty(alias)) {
|
||||||
|
val finalAlias = alias
|
||||||
|
Thread {
|
||||||
|
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 {
|
||||||
|
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: Field = 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReceivedError(view: WebView, errorCode: Int, description: String, failingUrl: String) {
|
||||||
|
super.onReceivedError(view, errorCode, description, failingUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binding.webview.loadUrl("$baseUrl/index.php/login/flow", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun dispose() {
|
||||||
|
if (userQueryDisposable != null && !userQueryDisposable!!.isDisposed) {
|
||||||
|
userQueryDisposable!!.dispose()
|
||||||
|
}
|
||||||
|
userQueryDisposable = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseAndLoginFromWebView(dataString: String) {
|
||||||
|
val loginData = parseLoginData(assembledPrefix, dataString)
|
||||||
|
if (loginData != null) {
|
||||||
|
dispose()
|
||||||
|
val currentUser = userUtils.currentUser
|
||||||
|
var messageType: ApplicationWideMessageHolder.MessageType? = null
|
||||||
|
if (!isPasswordUpdate && userUtils.getIfUserWithUsernameAndServer(loginData.username, baseUrl)) {
|
||||||
|
messageType = ApplicationWideMessageHolder.MessageType.ACCOUNT_UPDATED_NOT_ADDED
|
||||||
|
}
|
||||||
|
if (userUtils.checkIfUserIsScheduledForDeletion(loginData.username, baseUrl)) {
|
||||||
|
ApplicationWideMessageHolder.getInstance().setMessageType(
|
||||||
|
ApplicationWideMessageHolder.MessageType.ACCOUNT_SCHEDULED_FOR_DELETION
|
||||||
|
)
|
||||||
|
if (!isPasswordUpdate) {
|
||||||
|
router.popToRoot()
|
||||||
|
} else {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
router.pushController(
|
||||||
|
RouterTransaction.with(AccountVerificationController(bundle))
|
||||||
|
.pushChangeHandler(HorizontalChangeHandler())
|
||||||
|
.popChangeHandler(HorizontalChangeHandler())
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
if (isPasswordUpdate) {
|
||||||
|
if (currentUser != null) {
|
||||||
|
userQueryDisposable = userUtils.createOrUpdateUser(
|
||||||
|
null,
|
||||||
|
loginData.token,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
"",
|
||||||
|
java.lang.Boolean.TRUE,
|
||||||
|
null,
|
||||||
|
currentUser.id,
|
||||||
|
null,
|
||||||
|
appPreferences!!.temporaryClientCertAlias,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
{
|
||||||
|
if (finalMessageType != null) {
|
||||||
|
ApplicationWideMessageHolder.getInstance().setMessageType(finalMessageType)
|
||||||
|
}
|
||||||
|
val data = Data.Builder().putString(
|
||||||
|
PushRegistrationWorker.ORIGIN,
|
||||||
|
"WebViewLoginController#parseAndLoginFromWebView"
|
||||||
|
).build()
|
||||||
|
val pushRegistrationWork = OneTimeWorkRequest.Builder(
|
||||||
|
PushRegistrationWorker::class.java
|
||||||
|
)
|
||||||
|
.setInputData(data)
|
||||||
|
.build()
|
||||||
|
WorkManager.getInstance().enqueue(pushRegistrationWork)
|
||||||
|
router.popCurrentController()
|
||||||
|
},
|
||||||
|
{ dispose() }
|
||||||
|
) { dispose() }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (finalMessageType != null) {
|
||||||
|
// FIXME when the user registers a new account that was setup before (aka
|
||||||
|
// ApplicationWideMessageHolder.MessageType.ACCOUNT_UPDATED_NOT_ADDED)
|
||||||
|
// The token is not updated in the database and therefor the account not visible/usable
|
||||||
|
ApplicationWideMessageHolder.getInstance().setMessageType(finalMessageType)
|
||||||
|
}
|
||||||
|
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: String = dataString.substring(prefix.length)
|
||||||
|
val values: Array<String> = data.split("&").toTypedArray()
|
||||||
|
if (values.size != PARAMETER_COUNT) {
|
||||||
|
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 onAttach(view: View) {
|
||||||
|
super.onAttach(view)
|
||||||
|
if (activity != null && resources != null) {
|
||||||
|
DisplayUtils.applyColorToStatusBar(
|
||||||
|
activity,
|
||||||
|
ResourcesCompat.getColor(resources!!, R.color.colorPrimary, null)
|
||||||
|
)
|
||||||
|
DisplayUtils.applyColorToNavigationBar(
|
||||||
|
activity!!.window,
|
||||||
|
ResourcesCompat.getColor(resources!!, R.color.colorPrimary, null)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView(view: View) {
|
||||||
|
super.onDestroyView(view)
|
||||||
|
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
sharedApplication!!.componentApplication.inject(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val appBarLayoutType: AppBarLayoutType
|
||||||
|
get() = AppBarLayoutType.EMPTY
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "WebViewLoginController"
|
||||||
|
private const val PROTOCOL_SUFFIX = "://"
|
||||||
|
private const val LOGIN_URL_DATA_KEY_VALUE_SEPARATOR = ":"
|
||||||
|
private const val PARAMETER_COUNT = 3
|
||||||
|
}
|
||||||
|
}
|
|
@ -94,7 +94,7 @@ class EntryMenuController(args: Bundle) :
|
||||||
ApplicationWideMessageHolder.getInstance().setMessageType(null)
|
ApplicationWideMessageHolder.getInstance().setMessageType(null)
|
||||||
if (binding.okButton.isEnabled) {
|
if (binding.okButton.isEnabled) {
|
||||||
binding.okButton.isEnabled = false
|
binding.okButton.isEnabled = false
|
||||||
binding.okButton.alpha = 0.7f
|
binding.okButton.alpha = OPACITY_BUTTON_DISABLED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -130,20 +130,20 @@ class EntryMenuController(args: Bundle) :
|
||||||
if (conversation!!.getName() == null || !conversation!!.getName().equals(s.toString())) {
|
if (conversation!!.getName() == null || !conversation!!.getName().equals(s.toString())) {
|
||||||
if (!binding.okButton.isEnabled) {
|
if (!binding.okButton.isEnabled) {
|
||||||
binding.okButton.isEnabled = true
|
binding.okButton.isEnabled = true
|
||||||
binding.okButton.alpha = 1.0f
|
binding.okButton.alpha = OPACITY_ENABLED
|
||||||
}
|
}
|
||||||
binding.textInputLayout.isErrorEnabled = false
|
binding.textInputLayout.isErrorEnabled = false
|
||||||
} else {
|
} else {
|
||||||
if (binding.okButton.isEnabled) {
|
if (binding.okButton.isEnabled) {
|
||||||
binding.okButton.isEnabled = false
|
binding.okButton.isEnabled = false
|
||||||
binding.okButton.alpha = 0.38f
|
binding.okButton.alpha = OPACITY_DISABLED
|
||||||
}
|
}
|
||||||
binding.textInputLayout.error = resources?.getString(R.string.nc_call_name_is_same)
|
binding.textInputLayout.error = resources?.getString(R.string.nc_call_name_is_same)
|
||||||
}
|
}
|
||||||
} else if (operation !== ConversationOperationEnum.OPS_CODE_GET_AND_JOIN_ROOM) {
|
} else if (operation !== ConversationOperationEnum.OPS_CODE_GET_AND_JOIN_ROOM) {
|
||||||
if (!binding.okButton.isEnabled) {
|
if (!binding.okButton.isEnabled) {
|
||||||
binding.okButton.isEnabled = true
|
binding.okButton.isEnabled = true
|
||||||
binding.okButton.alpha = 1.0f
|
binding.okButton.alpha = OPACITY_ENABLED
|
||||||
}
|
}
|
||||||
binding.textInputLayout.isErrorEnabled = false
|
binding.textInputLayout.isErrorEnabled = false
|
||||||
} else if (
|
} else if (
|
||||||
|
@ -152,20 +152,20 @@ class EntryMenuController(args: Bundle) :
|
||||||
) {
|
) {
|
||||||
if (!binding.okButton.isEnabled) {
|
if (!binding.okButton.isEnabled) {
|
||||||
binding.okButton.isEnabled = true
|
binding.okButton.isEnabled = true
|
||||||
binding.okButton.alpha = 1.0f
|
binding.okButton.alpha = OPACITY_ENABLED
|
||||||
}
|
}
|
||||||
binding.textInputLayout.isErrorEnabled = false
|
binding.textInputLayout.isErrorEnabled = false
|
||||||
} else {
|
} else {
|
||||||
if (binding.okButton.isEnabled) {
|
if (binding.okButton.isEnabled) {
|
||||||
binding.okButton.isEnabled = false
|
binding.okButton.isEnabled = false
|
||||||
binding.okButton.alpha = 0.38f
|
binding.okButton.alpha = OPACITY_DISABLED
|
||||||
}
|
}
|
||||||
binding.textInputLayout.error = resources?.getString(R.string.nc_wrong_link)
|
binding.textInputLayout.error = resources?.getString(R.string.nc_wrong_link)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (binding.okButton.isEnabled) {
|
if (binding.okButton.isEnabled) {
|
||||||
binding.okButton.isEnabled = false
|
binding.okButton.isEnabled = false
|
||||||
binding.okButton.alpha = 0.38f
|
binding.okButton.alpha = OPACITY_DISABLED
|
||||||
}
|
}
|
||||||
binding.textInputLayout.isErrorEnabled = false
|
binding.textInputLayout.isErrorEnabled = false
|
||||||
}
|
}
|
||||||
|
@ -334,5 +334,8 @@ class EntryMenuController(args: Bundle) :
|
||||||
ConversationOperationEnum.OPS_CODE_SET_PASSWORD,
|
ConversationOperationEnum.OPS_CODE_SET_PASSWORD,
|
||||||
ConversationOperationEnum.OPS_CODE_SHARE_LINK
|
ConversationOperationEnum.OPS_CODE_SHARE_LINK
|
||||||
)
|
)
|
||||||
|
const val OPACITY_DISABLED = 0.38f
|
||||||
|
const val OPACITY_BUTTON_DISABLED = 0.7f
|
||||||
|
const val OPACITY_ENABLED = 1.0f
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,6 +49,7 @@ import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedA
|
||||||
import com.nextcloud.talk.models.json.search.ContactsByNumberOverall
|
import com.nextcloud.talk.models.json.search.ContactsByNumberOverall
|
||||||
import com.nextcloud.talk.utils.ApiUtils
|
import com.nextcloud.talk.utils.ApiUtils
|
||||||
import com.nextcloud.talk.utils.ContactUtils
|
import com.nextcloud.talk.utils.ContactUtils
|
||||||
|
import com.nextcloud.talk.utils.DateConstants
|
||||||
import com.nextcloud.talk.utils.database.user.UserUtils
|
import com.nextcloud.talk.utils.database.user.UserUtils
|
||||||
import com.nextcloud.talk.utils.preferences.AppPreferences
|
import com.nextcloud.talk.utils.preferences.AppPreferences
|
||||||
import io.reactivex.Observer
|
import io.reactivex.Observer
|
||||||
|
@ -97,7 +98,12 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar
|
||||||
// Check if run already at the date
|
// Check if run already at the date
|
||||||
val force = inputData.getBoolean(KEY_FORCE, false)
|
val force = inputData.getBoolean(KEY_FORCE, false)
|
||||||
if (!force) {
|
if (!force) {
|
||||||
if (System.currentTimeMillis() - appPreferences.getPhoneBookIntegrationLastRun(0L) < 24 * 60 * 60 * 1000) {
|
if (System.currentTimeMillis() - appPreferences.getPhoneBookIntegrationLastRun(0L) <
|
||||||
|
DateConstants.DAYS_DIVIDER *
|
||||||
|
DateConstants.HOURS_DIVIDER *
|
||||||
|
DateConstants.MINUTES_DIVIDER *
|
||||||
|
DateConstants.SECOND_DIVIDER
|
||||||
|
) {
|
||||||
Log.d(TAG, "Already run within last 24h")
|
Log.d(TAG, "Already run within last 24h")
|
||||||
return Result.success()
|
return Result.success()
|
||||||
}
|
}
|
||||||
|
|
|
@ -103,8 +103,8 @@ class DownloadFileToCacheWorker(val context: Context, workerParameters: WorkerPa
|
||||||
}
|
}
|
||||||
|
|
||||||
var count: Int
|
var count: Int
|
||||||
val data = ByteArray(1024 * 4)
|
val data = ByteArray(BYTE_UNIT_DIVIDER * DATA_BYTES)
|
||||||
val bis: InputStream = BufferedInputStream(body.byteStream(), 1024 * 8)
|
val bis: InputStream = BufferedInputStream(body.byteStream(), BYTE_UNIT_DIVIDER * DOWNLOAD_STREAM_SIZE)
|
||||||
val outputFile = File(context.cacheDir, fileName + "_")
|
val outputFile = File(context.cacheDir, fileName + "_")
|
||||||
val output: OutputStream = FileOutputStream(outputFile)
|
val output: OutputStream = FileOutputStream(outputFile)
|
||||||
var total: Long = 0
|
var total: Long = 0
|
||||||
|
@ -116,9 +116,9 @@ class DownloadFileToCacheWorker(val context: Context, workerParameters: WorkerPa
|
||||||
while (count != -1) {
|
while (count != -1) {
|
||||||
if (totalFileSize > -1) {
|
if (totalFileSize > -1) {
|
||||||
total += count.toLong()
|
total += count.toLong()
|
||||||
val progress = (total * 100 / totalFileSize).toInt()
|
val progress = (total * COMPLETE_PERCENTAGE / totalFileSize).toInt()
|
||||||
val currentTime = System.currentTimeMillis() - startTime
|
val currentTime = System.currentTimeMillis() - startTime
|
||||||
if (currentTime > 50 * timeCount) {
|
if (currentTime > PROGRESS_THRESHOLD * timeCount) {
|
||||||
setProgressAsync(Data.Builder().putInt(PROGRESS, progress).build())
|
setProgressAsync(Data.Builder().putInt(PROGRESS, progress).build())
|
||||||
timeCount++
|
timeCount++
|
||||||
}
|
}
|
||||||
|
@ -156,5 +156,10 @@ class DownloadFileToCacheWorker(val context: Context, workerParameters: WorkerPa
|
||||||
const val KEY_FILE_SIZE = "KEY_FILE_SIZE"
|
const val KEY_FILE_SIZE = "KEY_FILE_SIZE"
|
||||||
const val PROGRESS = "PROGRESS"
|
const val PROGRESS = "PROGRESS"
|
||||||
const val SUCCESS = "SUCCESS"
|
const val SUCCESS = "SUCCESS"
|
||||||
|
const val BYTE_UNIT_DIVIDER = 1024
|
||||||
|
const val DATA_BYTES = 4
|
||||||
|
const val DOWNLOAD_STREAM_SIZE = 8
|
||||||
|
const val COMPLETE_PERCENTAGE = 100
|
||||||
|
const val PROGRESS_THRESHOLD = 50
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,7 @@ import java.util.Arrays
|
||||||
object AccountUtils {
|
object AccountUtils {
|
||||||
|
|
||||||
private const val TAG = "AccountUtils"
|
private const val TAG = "AccountUtils"
|
||||||
|
private const val MIN_SUPPORTED_FILES_APP_VERSION = 30060151
|
||||||
|
|
||||||
fun findAccounts(userEntitiesList: List<UserEntity>): List<Account> {
|
fun findAccounts(userEntitiesList: List<UserEntity>): List<Account> {
|
||||||
val context = NextcloudTalkApplication.sharedApplication!!.applicationContext
|
val context = NextcloudTalkApplication.sharedApplication!!.applicationContext
|
||||||
|
@ -110,7 +111,7 @@ object AccountUtils {
|
||||||
val pm = context.packageManager
|
val pm = context.packageManager
|
||||||
try {
|
try {
|
||||||
val packageInfo = pm.getPackageInfo(context.getString(R.string.nc_import_accounts_from), 0)
|
val packageInfo = pm.getPackageInfo(context.getString(R.string.nc_import_accounts_from), 0)
|
||||||
if (packageInfo.versionCode >= 30060151) {
|
if (packageInfo.versionCode >= MIN_SUPPORTED_FILES_APP_VERSION) {
|
||||||
val ownSignatures = pm.getPackageInfo(context.packageName, PackageManager.GET_SIGNATURES).signatures
|
val ownSignatures = pm.getPackageInfo(context.packageName, PackageManager.GET_SIGNATURES).signatures
|
||||||
val filesAppSignatures = pm.getPackageInfo(
|
val filesAppSignatures = pm.getPackageInfo(
|
||||||
context.getString(R.string.nc_import_accounts_from),
|
context.getString(R.string.nc_import_accounts_from),
|
||||||
|
|
30
app/src/main/java/com/nextcloud/talk/utils/DateConstants.kt
Normal file
30
app/src/main/java/com/nextcloud/talk/utils/DateConstants.kt
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
* Nextcloud Talk application
|
||||||
|
*
|
||||||
|
* @author Andy Scherzinger
|
||||||
|
* Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
|
||||||
|
*
|
||||||
|
* 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.utils
|
||||||
|
|
||||||
|
class DateConstants {
|
||||||
|
companion object {
|
||||||
|
const val SECOND_DIVIDER = 1000
|
||||||
|
const val MINUTES_DIVIDER = 60
|
||||||
|
const val HOURS_DIVIDER = 60
|
||||||
|
const val DAYS_DIVIDER = 24
|
||||||
|
}
|
||||||
|
}
|
|
@ -35,10 +35,6 @@ import kotlin.math.roundToInt
|
||||||
object DateUtils {
|
object DateUtils {
|
||||||
|
|
||||||
private const val TIMESTAMP_CORRECTION_MULTIPLIER = 1000
|
private const val TIMESTAMP_CORRECTION_MULTIPLIER = 1000
|
||||||
private const val SECOND_DIVIDER = 1000
|
|
||||||
private const val MINUTES_DIVIDER = 60
|
|
||||||
private const val HOURS_DIVIDER = 60
|
|
||||||
private const val DAYS_DIVIDER = 24
|
|
||||||
|
|
||||||
fun getLocalDateTimeStringFromTimestamp(timestamp: Long): String {
|
fun getLocalDateTimeStringFromTimestamp(timestamp: Long): String {
|
||||||
val cal = Calendar.getInstance()
|
val cal = Calendar.getInstance()
|
||||||
|
@ -63,9 +59,9 @@ object DateUtils {
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
val fmt = RelativeDateTimeFormatter.getInstance()
|
val fmt = RelativeDateTimeFormatter.getInstance()
|
||||||
val timeLeftMillis = timestamp * TIMESTAMP_CORRECTION_MULTIPLIER - System.currentTimeMillis()
|
val timeLeftMillis = timestamp * TIMESTAMP_CORRECTION_MULTIPLIER - System.currentTimeMillis()
|
||||||
val minutes = timeLeftMillis.toDouble() / SECOND_DIVIDER / MINUTES_DIVIDER
|
val minutes = timeLeftMillis.toDouble() / DateConstants.SECOND_DIVIDER / DateConstants.MINUTES_DIVIDER
|
||||||
val hours = minutes / HOURS_DIVIDER
|
val hours = minutes / DateConstants.HOURS_DIVIDER
|
||||||
val days = hours / DAYS_DIVIDER
|
val days = hours / DateConstants.DAYS_DIVIDER
|
||||||
|
|
||||||
val minutesInt = minutes.roundToInt()
|
val minutesInt = minutes.roundToInt()
|
||||||
val hoursInt = hours.roundToInt()
|
val hoursInt = hours.roundToInt()
|
||||||
|
|
|
@ -35,7 +35,7 @@ class SSLSocketFactoryCompat(
|
||||||
var cipherSuites: Array<String>? = null
|
var cipherSuites: Array<String>? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
if (Build.VERSION.SDK_INT >= 23) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
// Since Android 6.0 (API level 23),
|
// Since Android 6.0 (API level 23),
|
||||||
// - TLSv1.1 and TLSv1.2 is enabled by default
|
// - TLSv1.1 and TLSv1.2 is enabled by default
|
||||||
// - SSLv3 is disabled by default
|
// - SSLv3 is disabled by default
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
build:
|
build:
|
||||||
maxIssues: 150
|
maxIssues: 99
|
||||||
weights:
|
weights:
|
||||||
# complexity: 2
|
# complexity: 2
|
||||||
# LongParameterList: 1
|
# LongParameterList: 1
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
446
|
438
|
|
@ -1,2 +1,2 @@
|
||||||
DO NOT TOUCH; GENERATED BY DRONE
|
DO NOT TOUCH; GENERATED BY DRONE
|
||||||
<span class="mdl-layout-title">Lint Report: 1 error and 172 warnings</span>
|
<span class="mdl-layout-title">Lint Report: 1 error and 168 warnings</span>
|
||||||
|
|
Loading…
Reference in a new issue