diff --git a/app/build.gradle b/app/build.gradle index 2e7549fb1..be6d6623c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -113,7 +113,9 @@ android { packagingOptions { exclude 'META-INF/LICENSE.txt' exclude 'META-INF/LICENSE' + exclude 'META-INF/NOTICE.txt' exclude 'META-INF/NOTICE' + exclude 'META-INF/DEPENDENCIES' exclude 'META-INF/rxjava.properties' } @@ -267,7 +269,10 @@ dependencies { implementation 'com.github.Kennyc1012:BottomSheet:2.4.1' implementation 'com.github.nextcloud:PopupBubble:master-SNAPSHOT' implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' - implementation 'eu.medsea.mimeutil:mime-util:2.1.3' + + implementation('eu.medsea.mimeutil:mime-util:2.1.3', { + exclude group: 'org.slf4j' + }) implementation "com.afollestad.material-dialogs:core:${materialDialogsVersion}" implementation "com.afollestad.material-dialogs:datetime:${materialDialogsVersion}" @@ -286,6 +291,12 @@ dependencies { implementation 'com.github.tobiaskaminsky:ImagePicker:extraFile-SNAPSHOT' implementation 'com.elyeproj.libraries:loaderviewlibrary:2.0.0' + implementation 'org.osmdroid:osmdroid-android:6.1.10' + implementation ('fr.dudie:nominatim-api:3.4', { + //noinspection DuplicatePlatformClasses + exclude group: 'org.apache.httpcomponents', module: 'httpclient' + }) + testImplementation 'junit:junit:4.13.2' testImplementation 'org.mockito:mockito-core:3.11.0' testImplementation "org.powermock:powermock-core:${powermockVersion}" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b92c45f31..acca95c7a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -70,6 +70,9 @@ + + + + + + + + + + +
+ + + + \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/talk/adapters/GeocodingAdapter.kt b/app/src/main/java/com/nextcloud/talk/adapters/GeocodingAdapter.kt new file mode 100644 index 000000000..698d319b2 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/GeocodingAdapter.kt @@ -0,0 +1,58 @@ +/* + * Nextcloud Talk application + * + * @author Marcel Hibbe + * Copyright (C) 2021 Marcel Hibbe + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.adapters + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.BaseAdapter +import android.widget.TextView +import com.nextcloud.talk.R +import fr.dudie.nominatim.model.Address + +class GeocodingAdapter(context: Context, val dataSource: List
) : BaseAdapter() { + + private val inflater: LayoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater + + override fun getCount(): Int { + return dataSource.size + } + + override fun getItem(position: Int): Any { + return dataSource[position] + } + + override fun getItemId(position: Int): Long { + return position.toLong() + } + + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val rowView = inflater.inflate(R.layout.geocoding_item, parent, false) + + val nameView = rowView.findViewById(R.id.name) as TextView + + val address = getItem(position) as Address + nameView.text = address.displayName + + return rowView + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt new file mode 100644 index 000000000..8be30ca94 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingLocationMessageViewHolder.kt @@ -0,0 +1,269 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * @author Marcel Hibbe + * @author Andy Scherzinger + * Copyright (C) 2021 Andy Scherzinger + * Copyright (C) 2021 Marcel Hibbe + * Copyright (C) 2017-2018 Mario Danic + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.adapters.messages + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.graphics.drawable.Drawable +import android.graphics.drawable.LayerDrawable +import android.net.Uri +import android.text.TextUtils +import android.util.Log +import android.util.TypedValue +import android.view.MotionEvent +import android.view.View +import android.webkit.WebView +import android.webkit.WebViewClient +import android.widget.Toast +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.view.ViewCompat +import autodagger.AutoInjector +import coil.load +import com.amulyakhare.textdrawable.TextDrawable +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.databinding.ItemCustomIncomingLocationMessageBinding +import com.nextcloud.talk.models.json.chat.ChatMessage +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.DisplayUtils +import com.nextcloud.talk.utils.preferences.AppPreferences +import com.stfalcon.chatkit.messages.MessageHolders +import java.net.URLEncoder +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class IncomingLocationMessageViewHolder(incomingView: View) : MessageHolders +.IncomingTextMessageViewHolder(incomingView) { + private val binding: ItemCustomIncomingLocationMessageBinding = + ItemCustomIncomingLocationMessageBinding.bind(itemView) + + var locationLon: String? = "" + var locationLat: String? = "" + var locationName: String? = "" + var locationGeoLink: String? = "" + + @JvmField + @Inject + var context: Context? = null + + @JvmField + @Inject + var appPreferences: AppPreferences? = null + + @SuppressLint("SetTextI18n") + override fun onBind(message: ChatMessage) { + super.onBind(message) + sharedApplication!!.componentApplication.inject(this) + + setAvatarAndAuthorOnMessageItem(message) + + colorizeMessageBubble(message) + + itemView.isSelected = false + binding.messageTime.setTextColor(context?.resources!!.getColor(R.color.warm_grey_four)) + + val textSize = context?.resources!!.getDimension(R.dimen.chat_text_size) + binding.messageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize) + binding.messageText.text = message.text + binding.messageText.isEnabled = false + + // parent message handling + setParentMessageDataOnMessageItem(message) + + // geo-location + setLocationDataOnMessageItem(message) + } + + private fun setAvatarAndAuthorOnMessageItem(message: ChatMessage) { + val author: String = message.actorDisplayName + if (!TextUtils.isEmpty(author)) { + binding.messageAuthor.text = author + } else { + binding.messageAuthor.setText(R.string.nc_nick_guest) + } + + if (!message.isGrouped && !message.isOneToOneConversation) { + binding.messageUserAvatar.visibility = View.VISIBLE + if (message.actorType == "guests") { + // do nothing, avatar is set + } else if (message.actorType == "bots" && message.actorId == "changelog") { + val layers = arrayOfNulls(2) + layers[0] = AppCompatResources.getDrawable(context!!, R.drawable.ic_launcher_background) + layers[1] = AppCompatResources.getDrawable(context!!, R.drawable.ic_launcher_foreground) + val layerDrawable = LayerDrawable(layers) + binding.messageUserAvatar.setImageDrawable(DisplayUtils.getRoundedDrawable(layerDrawable)) + } else if (message.actorType == "bots") { + val drawable = TextDrawable.builder() + .beginConfig() + .bold() + .endConfig() + .buildRound( + ">", + context!!.resources.getColor(R.color.black) + ) + binding.messageUserAvatar.visibility = View.VISIBLE + 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) { + val resources = itemView.resources + + var bubbleResource = R.drawable.shape_incoming_message + + if (message.isGrouped) { + bubbleResource = R.drawable.shape_grouped_incoming_message + } + + val bgBubbleColor = if (message.isDeleted) { + resources.getColor(R.color.bg_message_list_incoming_bubble_deleted) + } else { + resources.getColor(R.color.bg_message_list_incoming_bubble) + } + val bubbleDrawable = DisplayUtils.getMessageSelector( + bgBubbleColor, + resources.getColor(R.color.transparent), + bgBubbleColor, bubbleResource + ) + ViewCompat.setBackground(bubble, bubbleDrawable) + } + + private fun setParentMessageDataOnMessageItem(message: ChatMessage) { + 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 = parentChatMessage.actorDisplayName + ?: context!!.getText(R.string.nc_nick_guest) + binding.messageQuote.quotedMessage.text = parentChatMessage.text + + binding.messageQuote.quotedMessageAuthor + .setTextColor(context!!.resources.getColor(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 + } + } + + @SuppressLint("SetJavaScriptEnabled", "ClickableViewAccessibility") + private fun setLocationDataOnMessageItem(message: ChatMessage) { + if (message.messageParameters != null && message.messageParameters.size > 0) { + for (key in message.messageParameters.keys) { + val individualHashMap: Map = message.messageParameters[key]!! + if (individualHashMap["type"] == "geo-location") { + locationLon = individualHashMap["longitude"] + locationLat = individualHashMap["latitude"] + locationName = individualHashMap["name"] + locationGeoLink = individualHashMap["id"] + } + } + } + + binding.webview.settings?.javaScriptEnabled = true + + binding.webview.webViewClient = object : WebViewClient() { + override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean { + return if (url != null && (url.startsWith("http://") || url.startsWith("https://")) + ) { + view?.context?.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) + true + } else { + false + } + } + } + + val urlStringBuffer = StringBuffer("file:///android_asset/leafletMapMessagePreview.html") + urlStringBuffer.append( + "?mapProviderUrl=" + URLEncoder.encode(context!!.getString(R.string.osm_tile_server_url)) + ) + urlStringBuffer.append( + "&mapProviderAttribution=" + URLEncoder.encode(context!!.getString(R.string.osm_tile_server_attributation)) + ) + urlStringBuffer.append("&locationLat=" + URLEncoder.encode(locationLat)) + urlStringBuffer.append("&locationLon=" + URLEncoder.encode(locationLon)) + urlStringBuffer.append("&locationName=" + URLEncoder.encode(locationName)) + urlStringBuffer.append("&locationGeoLink=" + URLEncoder.encode(locationGeoLink)) + + binding.webview.loadUrl(urlStringBuffer.toString()) + + binding.webview.setOnTouchListener(object : View.OnTouchListener { + override fun onTouch(v: View?, event: MotionEvent?): Boolean { + when (event?.action) { + MotionEvent.ACTION_UP -> openGeoLink() + } + + return v?.onTouchEvent(event) ?: true + } + }) + } + + private fun openGeoLink() { + if (!locationGeoLink.isNullOrEmpty()) { + val geoLinkWithMarker = addMarkerToGeoLink(locationGeoLink!!) + val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(geoLinkWithMarker)) + browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context!!.startActivity(browserIntent) + } else { + Toast.makeText(context, R.string.nc_common_error_sorry, Toast.LENGTH_LONG).show() + Log.e(TAG, "locationGeoLink was null or empty") + } + } + + private fun addMarkerToGeoLink(locationGeoLink: String): String { + return locationGeoLink.replace("geo:", "geo:0,0?q=") + } + + companion object { + private const val TAG = "LocInMessageView" + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPreviewMessageViewHolder.java b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPreviewMessageViewHolder.java new file mode 100644 index 000000000..2e181c1cb --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingPreviewMessageViewHolder.java @@ -0,0 +1,45 @@ +/* + * Nextcloud Talk application + * + * @author Andy Scherzinger + * Copyright (C) 2021 Andy Scherzinger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.adapters.messages; + +import android.view.View; +import android.widget.ProgressBar; + +import com.nextcloud.talk.databinding.ItemCustomIncomingPreviewMessageBinding; + +import androidx.emoji.widget.EmojiTextView; + +public class IncomingPreviewMessageViewHolder extends MagicPreviewMessageViewHolder { + private final ItemCustomIncomingPreviewMessageBinding binding; + + public IncomingPreviewMessageViewHolder(View itemView) { + super(itemView); + binding = ItemCustomIncomingPreviewMessageBinding.bind(itemView); + } + + public EmojiTextView getMessageText() { + return binding.messageText; + } + + public ProgressBar getProgressBar() { + return binding.progressBar; + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.kt index abf27d53a..a7b47438c 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicIncomingTextMessageViewHolder.kt @@ -2,6 +2,8 @@ * Nextcloud Talk application * * @author Mario Danic + * @author Andy Scherzinger + * Copyright (C) 2021 Andy Scherzinger * Copyright (C) 2017-2018 Mario Danic * * This program is free software: you can redistribute it and/or modify @@ -30,20 +32,14 @@ import android.text.SpannableString import android.text.TextUtils import android.util.TypedValue import android.view.View -import android.widget.ImageView -import android.widget.RelativeLayout -import android.widget.TextView import androidx.core.view.ViewCompat -import androidx.emoji.widget.EmojiTextView import autodagger.AutoInjector -import butterknife.BindView -import butterknife.ButterKnife import coil.load import com.amulyakhare.textdrawable.TextDrawable -import com.facebook.drawee.view.SimpleDraweeView import com.nextcloud.talk.R import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.databinding.ItemCustomIncomingTextMessageBinding import com.nextcloud.talk.models.json.chat.ChatMessage import com.nextcloud.talk.ui.recyclerview.MessageSwipeCallback import com.nextcloud.talk.utils.ApiUtils @@ -57,41 +53,7 @@ import javax.inject.Inject class MagicIncomingTextMessageViewHolder(itemView: View) : MessageHolders .IncomingTextMessageViewHolder(itemView) { - @JvmField - @BindView(R.id.messageAuthor) - var messageAuthor: EmojiTextView? = null - - @JvmField - @BindView(R.id.messageText) - var messageText: EmojiTextView? = null - - @JvmField - @BindView(R.id.messageUserAvatar) - var messageUserAvatarView: SimpleDraweeView? = null - - @JvmField - @BindView(R.id.messageTime) - var messageTimeView: TextView? = null - - @JvmField - @BindView(R.id.quotedChatMessageView) - var quotedChatMessageView: RelativeLayout? = null - - @JvmField - @BindView(R.id.quotedMessageAuthor) - var quotedUserName: EmojiTextView? = null - - @JvmField - @BindView(R.id.quotedMessageImage) - var quotedMessagePreview: ImageView? = null - - @JvmField - @BindView(R.id.quotedMessage) - var quotedMessage: EmojiTextView? = null - - @JvmField - @BindView(R.id.quoteColoredView) - var quoteColoredView: View? = null + private val binding: ItemCustomIncomingTextMessageBinding = ItemCustomIncomingTextMessageBinding.bind(itemView) @JvmField @Inject @@ -101,22 +63,18 @@ class MagicIncomingTextMessageViewHolder(itemView: View) : MessageHolders @Inject var appPreferences: AppPreferences? = null - init { - ButterKnife.bind(this, itemView) - } - override fun onBind(message: ChatMessage) { super.onBind(message) sharedApplication!!.componentApplication.inject(this) val author: String = message.actorDisplayName if (!TextUtils.isEmpty(author)) { - messageAuthor!!.text = author + binding.messageAuthor.text = author } else { - messageAuthor!!.setText(R.string.nc_nick_guest) + binding.messageAuthor.setText(R.string.nc_nick_guest) } if (!message.isGrouped && !message.isOneToOneConversation) { - messageUserAvatarView!!.visibility = View.VISIBLE + binding.messageUserAvatar.visibility = View.VISIBLE if (message.actorType == "guests") { // do nothing, avatar is set } else if (message.actorType == "bots" && message.actorId == "changelog") { @@ -124,7 +82,7 @@ class MagicIncomingTextMessageViewHolder(itemView: View) : MessageHolders layers[0] = context?.getDrawable(R.drawable.ic_launcher_background) layers[1] = context?.getDrawable(R.drawable.ic_launcher_foreground) val layerDrawable = LayerDrawable(layers) - messageUserAvatarView?.setImageDrawable(DisplayUtils.getRoundedDrawable(layerDrawable)) + binding.messageUserAvatar.setImageDrawable(DisplayUtils.getRoundedDrawable(layerDrawable)) } else if (message.actorType == "bots") { val drawable = TextDrawable.builder() .beginConfig() @@ -134,16 +92,16 @@ class MagicIncomingTextMessageViewHolder(itemView: View) : MessageHolders ">", context!!.resources.getColor(R.color.black) ) - messageUserAvatarView!!.visibility = View.VISIBLE - messageUserAvatarView?.setImageDrawable(drawable) + binding.messageUserAvatar.visibility = View.VISIBLE + binding.messageUserAvatar.setImageDrawable(drawable) } } else { if (message.isOneToOneConversation) { - messageUserAvatarView!!.visibility = View.GONE + binding.messageUserAvatar.visibility = View.GONE } else { - messageUserAvatarView!!.visibility = View.INVISIBLE + binding.messageUserAvatar.visibility = View.INVISIBLE } - messageAuthor!!.visibility = View.GONE + binding.messageAuthor.visibility = View.GONE } val resources = itemView.resources @@ -170,7 +128,7 @@ class MagicIncomingTextMessageViewHolder(itemView: View) : MessageHolders val messageParameters = message.messageParameters itemView.isSelected = false - messageTimeView!!.setTextColor(context?.resources!!.getColor(R.color.warm_grey_four)) + binding.messageTime.setTextColor(context?.resources!!.getColor(R.color.warm_grey_four)) var messageString: Spannable = SpannableString(message.text) @@ -187,7 +145,7 @@ class MagicIncomingTextMessageViewHolder(itemView: View) : MessageHolders ) { if (individualHashMap["id"] == message.activeUser!!.userId) { messageString = DisplayUtils.searchAndReplaceWithMentionSpan( - messageText!!.context, + binding.messageText.context, messageString, individualHashMap["id"]!!, individualHashMap["name"]!!, @@ -197,7 +155,7 @@ class MagicIncomingTextMessageViewHolder(itemView: View) : MessageHolders ) } else { messageString = DisplayUtils.searchAndReplaceWithMentionSpan( - messageText!!.context, + binding.messageText.context, messageString, individualHashMap["id"]!!, individualHashMap["name"]!!, @@ -217,43 +175,43 @@ class MagicIncomingTextMessageViewHolder(itemView: View) : MessageHolders } else if (TextMatchers.isMessageWithSingleEmoticonOnly(message.text)) { textSize = (textSize * 2.5).toFloat() itemView.isSelected = true - messageAuthor!!.visibility = View.GONE + binding.messageAuthor.visibility = View.GONE } - messageText!!.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize) - messageText!!.text = messageString + binding.messageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize) + binding.messageText.text = messageString // parent message handling - if (!message.isDeleted && message.parentMessage != null) { - var parentChatMessage = message.parentMessage + val parentChatMessage = message.parentMessage parentChatMessage.activeUser = message.activeUser parentChatMessage.imageUrl?.let { - quotedMessagePreview?.visibility = View.VISIBLE - quotedMessagePreview?.load(it) { + binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE + binding.messageQuote.quotedMessageImage.load(it) { addHeader( "Authorization", ApiUtils.getCredentials(message.activeUser.username, message.activeUser.token) ) } } ?: run { - quotedMessagePreview?.visibility = View.GONE + binding.messageQuote.quotedMessageImage.visibility = View.GONE } - quotedUserName?.text = parentChatMessage.actorDisplayName + binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName ?: context!!.getText(R.string.nc_nick_guest) - quotedMessage?.text = parentChatMessage.text + binding.messageQuote.quotedMessage.text = parentChatMessage.text - quotedUserName?.setTextColor(context!!.resources.getColor(R.color.textColorMaxContrast)) + binding.messageQuote.quotedMessageAuthor + .setTextColor(context!!.resources.getColor(R.color.textColorMaxContrast)) if (parentChatMessage.actorId?.equals(message.activeUser.userId) == true) { - quoteColoredView?.setBackgroundResource(R.color.colorPrimary) + binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.colorPrimary) } else { - quoteColoredView?.setBackgroundResource(R.color.textColorMaxContrast) + binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.textColorMaxContrast) } - quotedChatMessageView?.visibility = View.VISIBLE + binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE } else { - quotedChatMessageView?.visibility = View.GONE + binding.messageQuote.quotedChatMessageView.visibility = View.GONE } itemView.setTag(MessageSwipeCallback.REPLYABLE_VIEW_TAG, message.isReplyable) diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicOutcomingTextMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicOutcomingTextMessageViewHolder.kt index a573c4956..520e20294 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicOutcomingTextMessageViewHolder.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicOutcomingTextMessageViewHolder.kt @@ -2,6 +2,8 @@ * Nextcloud Talk application * * @author Mario Danic + * @author Andy Scherzinger + * Copyright (C) 2021 Andy Scherzinger * Copyright (C) 2017-2018 Mario Danic * * This program is free software: you can redistribute it and/or modify @@ -27,19 +29,14 @@ import android.text.Spannable import android.text.SpannableString import android.util.TypedValue import android.view.View -import android.widget.ImageView -import android.widget.RelativeLayout -import android.widget.TextView import androidx.core.view.ViewCompat -import androidx.emoji.widget.EmojiTextView import autodagger.AutoInjector -import butterknife.BindView -import butterknife.ButterKnife import coil.load import com.google.android.flexbox.FlexboxLayout import com.nextcloud.talk.R import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.databinding.ItemCustomOutcomingTextMessageBinding import com.nextcloud.talk.models.json.chat.ChatMessage import com.nextcloud.talk.models.json.chat.ReadStatus import com.nextcloud.talk.ui.recyclerview.MessageSwipeCallback @@ -53,57 +50,21 @@ import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) class MagicOutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessageViewHolder(itemView) { - @JvmField - @BindView(R.id.messageText) - var messageText: EmojiTextView? = null - - @JvmField - @BindView(R.id.messageTime) - var messageTimeView: TextView? = null - - @JvmField - @BindView(R.id.quotedChatMessageView) - var quotedChatMessageView: RelativeLayout? = null - - @JvmField - @BindView(R.id.quotedMessageAuthor) - var quotedUserName: EmojiTextView? = null - - @JvmField - @BindView(R.id.quotedMessageImage) - var quotedMessagePreview: ImageView? = null - - @JvmField - @BindView(R.id.quotedMessage) - var quotedMessage: EmojiTextView? = null - - @JvmField - @BindView(R.id.quoteColoredView) - var quoteColoredView: View? = null - - @JvmField - @BindView(R.id.checkMark) - var checkMark: ImageView? = null + private val binding: ItemCustomOutcomingTextMessageBinding = ItemCustomOutcomingTextMessageBinding.bind(itemView) + private val realView: View = itemView @JvmField @Inject var context: Context? = null - private val realView: View - - init { - ButterKnife.bind(this, itemView) - this.realView = itemView - } - override fun onBind(message: ChatMessage) { super.onBind(message) sharedApplication!!.componentApplication.inject(this) val messageParameters: HashMap>? = message.messageParameters var messageString: Spannable = SpannableString(message.text) realView.isSelected = false - messageTimeView!!.setTextColor(context!!.resources.getColor(R.color.white60)) - val layoutParams = messageTimeView!!.layoutParams as FlexboxLayout.LayoutParams + binding.messageTime.setTextColor(context!!.resources.getColor(R.color.white60)) + val layoutParams = binding.messageTime.layoutParams as FlexboxLayout.LayoutParams layoutParams.isWrapBefore = false var textSize = context!!.resources.getDimension(R.dimen.chat_text_size) if (messageParameters != null && messageParameters.size > 0) { @@ -115,7 +76,7 @@ class MagicOutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessage ) || individualHashMap["type"] == "call" ) { messageString = searchAndReplaceWithMentionSpan( - messageText!!.context, + binding.messageText.context, messageString, individualHashMap["id"]!!, individualHashMap["name"]!!, @@ -136,7 +97,7 @@ class MagicOutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessage } else if (TextMatchers.isMessageWithSingleEmoticonOnly(message.text)) { textSize = (textSize * 2.5).toFloat() layoutParams.isWrapBefore = true - messageTimeView!!.setTextColor(context!!.resources.getColor(R.color.warm_grey_four)) + binding.messageTime.setTextColor(context!!.resources.getColor(R.color.warm_grey_four)) realView.isSelected = true } val resources = sharedApplication!!.resources @@ -162,9 +123,9 @@ class MagicOutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessage ) ViewCompat.setBackground(bubble, bubbleDrawable) } - messageText!!.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize) - messageTimeView!!.layoutParams = layoutParams - messageText!!.text = messageString + binding.messageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize) + binding.messageTime.layoutParams = layoutParams + binding.messageText.text = messageString // parent message handling @@ -172,27 +133,29 @@ class MagicOutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessage var parentChatMessage = message.parentMessage parentChatMessage.activeUser = message.activeUser parentChatMessage.imageUrl?.let { - quotedMessagePreview?.visibility = View.VISIBLE - quotedMessagePreview?.load(it) { + binding.messageQuote.quotedMessageImage.visibility = View.VISIBLE + binding.messageQuote.quotedMessageImage.load(it) { addHeader( "Authorization", ApiUtils.getCredentials(message.activeUser.username, message.activeUser.token) ) } } ?: run { - quotedMessagePreview?.visibility = View.GONE + binding.messageQuote.quotedMessageImage.visibility = View.GONE } - quotedUserName?.text = parentChatMessage.actorDisplayName + binding.messageQuote.quotedMessageAuthor.text = parentChatMessage.actorDisplayName ?: context!!.getText(R.string.nc_nick_guest) - quotedMessage?.text = parentChatMessage.text - quotedMessage?.setTextColor(context!!.resources.getColor(R.color.nc_outcoming_text_default)) - quotedUserName?.setTextColor(context!!.resources.getColor(R.color.nc_grey)) + binding.messageQuote.quotedMessage.text = parentChatMessage.text + binding.messageQuote.quotedMessage.setTextColor( + context!!.resources.getColor(R.color.nc_outcoming_text_default) + ) + binding.messageQuote.quotedMessageAuthor.setTextColor(context!!.resources.getColor(R.color.nc_grey)) - quoteColoredView?.setBackgroundResource(R.color.white) + binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.white) - quotedChatMessageView?.visibility = View.VISIBLE + binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE } else { - quotedChatMessageView?.visibility = View.GONE + binding.messageQuote.quotedChatMessageView.visibility = View.GONE } val readStatusDrawableInt = when (message.readStatus) { @@ -210,11 +173,11 @@ class MagicOutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessage readStatusDrawableInt?.let { drawableInt -> context?.resources?.getDrawable(drawableInt, null)?.let { it.setColorFilter(context?.resources!!.getColor(R.color.white60), PorterDuff.Mode.SRC_ATOP) - checkMark?.setImageDrawable(it) + binding.checkMark.setImageDrawable(it) } } - checkMark?.setContentDescription(readStatusContentDescriptionString) + binding.checkMark.setContentDescription(readStatusContentDescriptionString) itemView.setTag(MessageSwipeCallback.REPLYABLE_VIEW_TAG, message.isReplyable) } diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.java b/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.java index 6f899c11a..c6ddc7433 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.java +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/MagicPreviewMessageViewHolder.java @@ -3,8 +3,10 @@ * * @author Mario Danic * @author Marcel Hibbe - * Copyright (C) 2017-2018 Mario Danic + * @author Andy Scherzinger + * Copyright (C) 2021 Andy Scherzinger * Copyright (C) 2021 Marcel Hibbe + * Copyright (C) 2017-2018 Mario Danic * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -35,6 +37,7 @@ import android.util.Log; import android.view.Gravity; import android.view.View; import android.widget.PopupMenu; +import android.widget.ProgressBar; import com.google.common.util.concurrent.ListenableFuture; import com.nextcloud.talk.R; @@ -70,8 +73,6 @@ import androidx.work.OneTimeWorkRequest; import androidx.work.WorkInfo; import androidx.work.WorkManager; import autodagger.AutoInjector; -import butterknife.BindView; -import butterknife.ButterKnife; import io.reactivex.Single; import io.reactivex.SingleObserver; import io.reactivex.annotations.NonNull; @@ -82,15 +83,10 @@ import okhttp3.OkHttpClient; import static com.nextcloud.talk.ui.recyclerview.MessageSwipeCallback.REPLYABLE_VIEW_TAG; @AutoInjector(NextcloudTalkApplication.class) -public class MagicPreviewMessageViewHolder extends MessageHolders.IncomingImageMessageViewHolder { +public abstract class MagicPreviewMessageViewHolder extends MessageHolders.IncomingImageMessageViewHolder { private static final String TAG = "PreviewMsgViewHolder"; - @BindView(R.id.messageText) - EmojiTextView messageText; - - View progressBar; - @Inject Context context; @@ -99,8 +95,6 @@ public class MagicPreviewMessageViewHolder extends MessageHolders.IncomingImageM public MagicPreviewMessageViewHolder(View itemView) { super(itemView); - ButterKnife.bind(this, itemView); - progressBar = itemView.findViewById(R.id.progress_bar); NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this); } @@ -131,7 +125,7 @@ public class MagicPreviewMessageViewHolder extends MessageHolders.IncomingImageM if (message.getMessageType() == ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE) { String fileName = message.getSelectedIndividualHashMap().get("name"); - messageText.setText(fileName); + getMessageText().setText(fileName); if (message.getSelectedIndividualHashMap().containsKey("mimetype")) { String mimetype = message.getSelectedIndividualHashMap().get("mimetype"); int drawableResourceId = DrawableUtils.INSTANCE.getDrawableResourceIdForMimeType(mimetype); @@ -165,7 +159,7 @@ public class MagicPreviewMessageViewHolder extends MessageHolders.IncomingImageM try { for (WorkInfo workInfo : workers.get()) { if (workInfo.getState() == WorkInfo.State.RUNNING || workInfo.getState() == WorkInfo.State.ENQUEUED) { - progressBar.setVisibility(View.VISIBLE); + getProgressBar().setVisibility(View.VISIBLE); String mimetype = message.getSelectedIndividualHashMap().get("mimetype"); @@ -177,13 +171,12 @@ public class MagicPreviewMessageViewHolder extends MessageHolders.IncomingImageM } catch (ExecutionException | InterruptedException e) { Log.e(TAG, "Error when checking if worker already exists", e); } - } else if (message.getMessageType() == ChatMessage.MessageType.SINGLE_LINK_GIPHY_MESSAGE) { - messageText.setText("GIPHY"); - DisplayUtils.setClickableString("GIPHY", "https://giphy.com", messageText); + getMessageText().setText("GIPHY"); + DisplayUtils.setClickableString("GIPHY", "https://giphy.com", getMessageText()); } else if (message.getMessageType() == ChatMessage.MessageType.SINGLE_LINK_TENOR_MESSAGE) { - messageText.setText("Tenor"); - DisplayUtils.setClickableString("Tenor", "https://tenor.com", messageText); + getMessageText().setText("Tenor"); + DisplayUtils.setClickableString("Tenor", "https://tenor.com", getMessageText()); } else { if (message.getMessageType().equals(ChatMessage.MessageType.SINGLE_LINK_IMAGE_MESSAGE)) { image.setOnClickListener(v -> { @@ -194,12 +187,16 @@ public class MagicPreviewMessageViewHolder extends MessageHolders.IncomingImageM } else { image.setOnClickListener(null); } - messageText.setText(""); + getMessageText().setText(""); } itemView.setTag(REPLYABLE_VIEW_TAG, message.isReplyable()); } + public abstract EmojiTextView getMessageText(); + + public abstract ProgressBar getProgressBar(); + private void openOrDownloadFile(ChatMessage message) { String filename = message.getSelectedIndividualHashMap().get("name"); String mimetype = message.getSelectedIndividualHashMap().get("mimetype"); @@ -389,7 +386,7 @@ public class MagicPreviewMessageViewHolder extends MessageHolders.IncomingImageM WorkManager.getInstance().enqueue(downloadWorker); - progressBar.setVisibility(View.VISIBLE); + getProgressBar().setVisibility(View.VISIBLE); WorkManager.getInstance(context).getWorkInfoByIdLiveData(downloadWorker.getId()).observeForever(workInfo -> { updateViewsByProgress(fileName, mimetype, workInfo); @@ -401,7 +398,7 @@ public class MagicPreviewMessageViewHolder extends MessageHolders.IncomingImageM case RUNNING: int progress = workInfo.getProgress().getInt(DownloadFileToCacheWorker.PROGRESS, -1); if (progress > -1) { - messageText.setText(String.format(context.getResources().getString(R.string.filename_progress), fileName, progress)); + getMessageText().setText(String.format(context.getResources().getString(R.string.filename_progress), fileName, progress)); } break; @@ -412,13 +409,13 @@ public class MagicPreviewMessageViewHolder extends MessageHolders.IncomingImageM Log.d(TAG, "file " + fileName + " was downloaded but it's not opened because view is not shown on" + " screen"); } - messageText.setText(fileName); - progressBar.setVisibility(View.GONE); + getMessageText().setText(fileName); + getProgressBar().setVisibility(View.GONE); break; case FAILED: - messageText.setText(fileName); - progressBar.setVisibility(View.GONE); + getMessageText().setText(fileName); + getProgressBar().setVisibility(View.GONE); break; default: // do nothing @@ -487,6 +484,5 @@ public class MagicPreviewMessageViewHolder extends MessageHolders.IncomingImageM Log.e(TAG, "Error reading file information", e); } }); - } } diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLocationMessageViewHolder.kt b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLocationMessageViewHolder.kt new file mode 100644 index 000000000..6ef0c283b --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingLocationMessageViewHolder.kt @@ -0,0 +1,250 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * @author Marcel Hibbe + * Copyright (C) 2017-2018 Mario Danic + * Copyright (C) 2021 Marcel Hibbe + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.adapters.messages + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.graphics.PorterDuff +import android.net.Uri +import android.util.Log +import android.util.TypedValue +import android.view.MotionEvent +import android.view.View +import android.webkit.WebView +import android.webkit.WebViewClient +import android.widget.Toast +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.view.ViewCompat +import autodagger.AutoInjector +import coil.load +import com.google.android.flexbox.FlexboxLayout +import com.nextcloud.talk.R +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication +import com.nextcloud.talk.databinding.ItemCustomOutcomingLocationMessageBinding +import com.nextcloud.talk.models.json.chat.ChatMessage +import com.nextcloud.talk.models.json.chat.ReadStatus +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.DisplayUtils +import com.stfalcon.chatkit.messages.MessageHolders +import java.net.URLEncoder +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class OutcomingLocationMessageViewHolder(incomingView: View) : MessageHolders +.OutcomingTextMessageViewHolder(incomingView) { + private val binding: ItemCustomOutcomingLocationMessageBinding = + ItemCustomOutcomingLocationMessageBinding.bind(itemView) + private val realView: View = itemView + + var locationLon: String? = "" + var locationLat: String? = "" + var locationName: String? = "" + var locationGeoLink: String? = "" + + @JvmField + @Inject + var context: Context? = null + + @SuppressLint("SetTextI18n") + override fun onBind(message: ChatMessage) { + super.onBind(message) + sharedApplication!!.componentApplication.inject(this) + + realView.isSelected = false + binding.messageTime.setTextColor(context!!.resources.getColor(R.color.white60)) + val layoutParams = binding.messageTime.layoutParams as FlexboxLayout.LayoutParams + layoutParams.isWrapBefore = false + + val textSize = context!!.resources.getDimension(R.dimen.chat_text_size) + + colorizeMessageBubble(message) + binding.messageText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize) + binding.messageTime.layoutParams = layoutParams + binding.messageText.text = message.text + binding.messageText.isEnabled = false + + // parent message handling + setParentMessageDataOnMessageItem(message) + + val readStatusDrawableInt = when (message.readStatus) { + ReadStatus.READ -> R.drawable.ic_check_all + ReadStatus.SENT -> R.drawable.ic_check + else -> null + } + + val readStatusContentDescriptionString = when (message.readStatus) { + ReadStatus.READ -> context?.resources?.getString(R.string.nc_message_read) + ReadStatus.SENT -> context?.resources?.getString(R.string.nc_message_sent) + else -> null + } + + readStatusDrawableInt?.let { drawableInt -> + AppCompatResources.getDrawable(context!!, drawableInt)?.let { + it.setColorFilter(context?.resources!!.getColor(R.color.white60), PorterDuff.Mode.SRC_ATOP) + binding.checkMark.setImageDrawable(it) + } + } + + binding.checkMark.setContentDescription(readStatusContentDescriptionString) + + // geo-location + setLocationDataOnMessageItem(message) + } + + @SuppressLint("SetJavaScriptEnabled", "ClickableViewAccessibility") + private fun setLocationDataOnMessageItem(message: ChatMessage) { + if (message.messageParameters != null && message.messageParameters.size > 0) { + for (key in message.messageParameters.keys) { + val individualHashMap: Map = message.messageParameters[key]!! + if (individualHashMap["type"] == "geo-location") { + locationLon = individualHashMap["longitude"] + locationLat = individualHashMap["latitude"] + locationName = individualHashMap["name"] + locationGeoLink = individualHashMap["id"] + } + } + } + + binding.webview.settings?.javaScriptEnabled = true + + binding.webview.webViewClient = object : WebViewClient() { + override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean { + return if (url != null && (url.startsWith("http://") || url.startsWith("https://")) + ) { + view?.context?.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) + true + } else { + false + } + } + } + + val urlStringBuffer = StringBuffer("file:///android_asset/leafletMapMessagePreview.html") + urlStringBuffer.append( + "?mapProviderUrl=" + URLEncoder.encode(context!!.getString(R.string.osm_tile_server_url)) + ) + urlStringBuffer.append( + "&mapProviderAttribution=" + URLEncoder.encode( + context!!.getString( + R.string + .osm_tile_server_attributation + ) + ) + ) + urlStringBuffer.append("&locationLat=" + URLEncoder.encode(locationLat)) + urlStringBuffer.append("&locationLon=" + URLEncoder.encode(locationLon)) + urlStringBuffer.append("&locationName=" + URLEncoder.encode(locationName)) + urlStringBuffer.append("&locationGeoLink=" + URLEncoder.encode(locationGeoLink)) + + binding.webview.loadUrl(urlStringBuffer.toString()) + + binding.webview.setOnTouchListener(object : View.OnTouchListener { + override fun onTouch(v: View?, event: MotionEvent?): Boolean { + when (event?.action) { + MotionEvent.ACTION_UP -> openGeoLink() + } + + return v?.onTouchEvent(event) ?: true + } + }) + } + + private fun setParentMessageDataOnMessageItem(message: ChatMessage) { + 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 = parentChatMessage.actorDisplayName + ?: context!!.getText(R.string.nc_nick_guest) + binding.messageQuote.quotedMessage.text = parentChatMessage.text + binding.messageQuote.quotedMessage.setTextColor( + context!!.resources.getColor(R.color.nc_outcoming_text_default) + ) + binding.messageQuote.quotedMessageAuthor.setTextColor(context!!.resources.getColor(R.color.nc_grey)) + + binding.messageQuote.quoteColoredView.setBackgroundResource(R.color.white) + + binding.messageQuote.quotedChatMessageView.visibility = View.VISIBLE + } else { + binding.messageQuote.quotedChatMessageView.visibility = View.GONE + } + } + + private fun colorizeMessageBubble(message: ChatMessage) { + val resources = sharedApplication!!.resources + val bgBubbleColor = if (message.isDeleted) { + resources.getColor(R.color.bg_message_list_outcoming_bubble_deleted) + } else { + resources.getColor(R.color.bg_message_list_outcoming_bubble) + } + if (message.isGrouped) { + val bubbleDrawable = DisplayUtils.getMessageSelector( + bgBubbleColor, + resources.getColor(R.color.transparent), + bgBubbleColor, + R.drawable.shape_grouped_outcoming_message + ) + ViewCompat.setBackground(bubble, bubbleDrawable) + } else { + val bubbleDrawable = DisplayUtils.getMessageSelector( + bgBubbleColor, + resources.getColor(R.color.transparent), + bgBubbleColor, + R.drawable.shape_outcoming_message + ) + ViewCompat.setBackground(bubble, bubbleDrawable) + } + } + + private fun openGeoLink() { + if (!locationGeoLink.isNullOrEmpty()) { + val geoLinkWithMarker = addMarkerToGeoLink(locationGeoLink!!) + val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(geoLinkWithMarker)) + browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context!!.startActivity(browserIntent) + } else { + Toast.makeText(context, R.string.nc_common_error_sorry, Toast.LENGTH_LONG).show() + Log.e(TAG, "locationGeoLink was null or empty") + } + } + + private fun addMarkerToGeoLink(locationGeoLink: String): String { + return locationGeoLink.replace("geo:", "geo:0,0?q=") + } + + companion object { + private const val TAG = "LocOutMessageView" + } +} diff --git a/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPreviewMessageViewHolder.java b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPreviewMessageViewHolder.java new file mode 100644 index 000000000..4b676c385 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingPreviewMessageViewHolder.java @@ -0,0 +1,45 @@ +/* + * Nextcloud Talk application + * + * @author Andy Scherzinger + * Copyright (C) 2021 Andy Scherzinger + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.adapters.messages; + +import android.view.View; +import android.widget.ProgressBar; + +import com.nextcloud.talk.databinding.ItemCustomOutcomingPreviewMessageBinding; + +import androidx.emoji.widget.EmojiTextView; + +public class OutcomingPreviewMessageViewHolder extends MagicPreviewMessageViewHolder { + private final ItemCustomOutcomingPreviewMessageBinding binding; + + public OutcomingPreviewMessageViewHolder(View itemView) { + super(itemView); + binding = ItemCustomOutcomingPreviewMessageBinding.bind(itemView); + } + + public EmojiTextView getMessageText() { + return binding.messageText; + } + + public ProgressBar getProgressBar() { + return binding.progressBar; + } +} diff --git a/app/src/main/java/com/nextcloud/talk/api/NcApi.java b/app/src/main/java/com/nextcloud/talk/api/NcApi.java index 2b16e13dd..85cb7b887 100644 --- a/app/src/main/java/com/nextcloud/talk/api/NcApi.java +++ b/app/src/main/java/com/nextcloud/talk/api/NcApi.java @@ -406,4 +406,12 @@ public interface NcApi { @GET Call downloadResizedImage(@Header("Authorization") String authorization, @Url String url); + + @FormUrlEncoded + @POST + Observable sendLocation(@Header("Authorization") String authorization, + @Url String url, + @Field("objectType") String objectType, + @Field("objectId") String objectId, + @Field("metaData") String metaData); } diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt b/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt index 18d560ef2..13975de25 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt @@ -78,11 +78,14 @@ import com.facebook.imagepipeline.image.CloseableImage import com.google.android.flexbox.FlexboxLayout import com.nextcloud.talk.R import com.nextcloud.talk.activities.MagicCallActivity +import com.nextcloud.talk.adapters.messages.IncomingLocationMessageViewHolder +import com.nextcloud.talk.adapters.messages.IncomingPreviewMessageViewHolder import com.nextcloud.talk.adapters.messages.MagicIncomingTextMessageViewHolder import com.nextcloud.talk.adapters.messages.MagicOutcomingTextMessageViewHolder -import com.nextcloud.talk.adapters.messages.MagicPreviewMessageViewHolder import com.nextcloud.talk.adapters.messages.MagicSystemMessageViewHolder import com.nextcloud.talk.adapters.messages.MagicUnreadNoticeMessageViewHolder +import com.nextcloud.talk.adapters.messages.OutcomingLocationMessageViewHolder +import com.nextcloud.talk.adapters.messages.OutcomingPreviewMessageViewHolder import com.nextcloud.talk.adapters.messages.TalkMessagesListAdapter import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication @@ -133,6 +136,7 @@ import com.otaliastudios.autocomplete.Autocomplete import com.stfalcon.chatkit.commons.ImageLoader import com.stfalcon.chatkit.commons.models.IMessage import com.stfalcon.chatkit.messages.MessageHolders +import com.stfalcon.chatkit.messages.MessageHolders.ContentChecker import com.stfalcon.chatkit.messages.MessagesListAdapter import com.stfalcon.chatkit.utils.DateFormatter import com.vanniktech.emoji.EmojiPopup @@ -163,7 +167,7 @@ class ChatController(args: Bundle) : MessagesListAdapter.OnLoadMoreListener, MessagesListAdapter.Formatter, MessagesListAdapter.OnMessageViewLongClickListener, - MessageHolders.ContentChecker { + ContentChecker { private val binding: ControllerChatBinding by viewBinding(ControllerChatBinding::bind) @Inject @@ -397,17 +401,20 @@ class ChatController(args: Bundle) : ) messageHolders.setIncomingImageConfig( - MagicPreviewMessageViewHolder::class.java, + IncomingPreviewMessageViewHolder::class.java, R.layout.item_custom_incoming_preview_message ) + messageHolders.setOutcomingImageConfig( - MagicPreviewMessageViewHolder::class.java, + OutcomingPreviewMessageViewHolder::class.java, R.layout.item_custom_outcoming_preview_message ) messageHolders.registerContentType( - CONTENT_TYPE_SYSTEM_MESSAGE, MagicSystemMessageViewHolder::class.java, - R.layout.item_system_message, MagicSystemMessageViewHolder::class.java, + CONTENT_TYPE_SYSTEM_MESSAGE, + MagicSystemMessageViewHolder::class.java, + R.layout.item_system_message, + MagicSystemMessageViewHolder::class.java, R.layout.item_system_message, this ) @@ -420,6 +427,15 @@ class ChatController(args: Bundle) : R.layout.item_date_header, this ) + messageHolders.registerContentType( + CONTENT_TYPE_LOCATION, + IncomingLocationMessageViewHolder::class.java, + R.layout.item_custom_incoming_location_message, + OutcomingLocationMessageViewHolder::class.java, + R.layout.item_custom_outcoming_location_message, + this + ) + var senderId = "" if (!conversationUser?.userId.equals("?")) { senderId = "users/" + conversationUser?.userId @@ -793,6 +809,18 @@ class ChatController(args: Bundle) : ) } + fun showShareLocationScreen() { + Log.d(TAG, "showShareLocationScreen") + + val bundle = Bundle() + bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomToken) + router.pushController( + RouterTransaction.with(LocationPickerController(bundle)) + .pushChangeHandler(HorizontalChangeHandler()) + .popChangeHandler(HorizontalChangeHandler()) + ) + } + private fun showConversationInfoScreen() { val bundle = Bundle() bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, conversationUser) @@ -1861,6 +1889,8 @@ class ChatController(args: Bundle) : if (message.hasFileAttachment()) return false + if (OBJECT_MESSAGE.equals(message.message)) return false + val isOlderThanSixHours = message .createdAt ?.before(Date(System.currentTimeMillis() - AGE_THREHOLD_FOR_DELETE_MESSAGE)) == true @@ -1878,8 +1908,9 @@ class ChatController(args: Bundle) : return true } - override fun hasContentFor(message: IMessage, type: Byte): Boolean { + override fun hasContentFor(message: ChatMessage, type: Byte): Boolean { return when (type) { + CONTENT_TYPE_LOCATION -> return message.isLocationMessage() CONTENT_TYPE_SYSTEM_MESSAGE -> !TextUtils.isEmpty(message.systemMessage) CONTENT_TYPE_UNREAD_NOTICE_MESSAGE -> message.id == "-1" else -> false @@ -1985,6 +2016,7 @@ class ChatController(args: Bundle) : private const val TAG = "ChatController" private const val CONTENT_TYPE_SYSTEM_MESSAGE: Byte = 1 private const val CONTENT_TYPE_UNREAD_NOTICE_MESSAGE: Byte = 2 + private const val CONTENT_TYPE_LOCATION: Byte = 3 private const val NEW_MESSAGES_POPUP_BUBBLE_DELAY: Long = 200 private const val POP_CURRENT_CONTROLLER_DELAY: Long = 100 private const val LOBBY_TIMER_DELAY: Long = 5000 @@ -1992,5 +2024,6 @@ class ChatController(args: Bundle) : private const val MESSAGE_MAX_LENGTH: Int = 1000 private const val AGE_THREHOLD_FOR_DELETE_MESSAGE: Int = 21600000 // (6 hours in millis = 6 * 3600 * 1000) private const val REQUEST_CODE_CHOOSE_FILE: Int = 555 + private const val OBJECT_MESSAGE: String = "{object}" } } diff --git a/app/src/main/java/com/nextcloud/talk/controllers/GeocodingController.kt b/app/src/main/java/com/nextcloud/talk/controllers/GeocodingController.kt new file mode 100644 index 000000000..332c69051 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/controllers/GeocodingController.kt @@ -0,0 +1,220 @@ +/* + * Nextcloud Talk application + * + * @author Marcel Hibbe + * Copyright (C) 2021 Marcel Hibbe + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.controllers + +import android.app.SearchManager +import android.content.Context +import android.os.Build +import android.os.Bundle +import android.text.InputType +import android.util.Log +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.inputmethod.EditorInfo +import android.widget.AdapterView +import android.widget.Toast +import androidx.appcompat.widget.SearchView +import androidx.core.view.MenuItemCompat +import androidx.preference.PreferenceManager +import autodagger.AutoInjector +import com.nextcloud.talk.R +import com.nextcloud.talk.adapters.GeocodingAdapter +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.controllers.base.NewBaseController +import com.nextcloud.talk.controllers.util.viewBinding +import com.nextcloud.talk.databinding.ControllerGeocodingBinding +import com.nextcloud.talk.utils.bundle.BundleKeys +import com.nextcloud.talk.utils.database.user.UserUtils +import fr.dudie.nominatim.client.TalkJsonNominatimClient +import fr.dudie.nominatim.model.Address +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import org.osmdroid.config.Configuration +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class GeocodingController(args: Bundle) : + NewBaseController( + R.layout.controller_geocoding, + args + ), + SearchView.OnQueryTextListener { + private val binding: ControllerGeocodingBinding by viewBinding(ControllerGeocodingBinding::bind) + + @Inject + lateinit var ncApi: NcApi + + @Inject + lateinit var userUtils: UserUtils + + @Inject + lateinit var okHttpClient: OkHttpClient + + var roomToken: String? + var nominatimClient: TalkJsonNominatimClient? = null + + var searchItem: MenuItem? = null + var searchView: SearchView? = null + var query: String? = null + + lateinit var adapter: GeocodingAdapter + private var geocodingResults: List
= ArrayList() + + constructor(args: Bundle, listener: LocationPickerController) : this(args) { + targetController = listener + } + + init { + setHasOptionsMenu(true) + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + Configuration.getInstance().load(context, PreferenceManager.getDefaultSharedPreferences(context)) + query = args.getString(BundleKeys.KEY_GEOCODING_QUERY) + roomToken = args.getString(BundleKeys.KEY_ROOM_TOKEN) + } + + private fun initAdapter(addresses: List
) { + adapter = GeocodingAdapter(binding.geocodingResults.context!!, addresses) + binding.geocodingResults.adapter = adapter + } + + override fun onAttach(view: View) { + super.onAttach(view) + + initAdapter(geocodingResults) + + initGeocoder() + if (!query.isNullOrEmpty()) { + searchLocation() + } else { + Log.e(TAG, "search string that was passed to GeocodingController was null or empty") + } + + binding.geocodingResults.onItemClickListener = AdapterView.OnItemClickListener { parent, view, position, id -> + val address: Address = adapter.getItem(position) as Address + val listener: GeocodingResultListener? = targetController as GeocodingResultListener? + listener?.receiveChosenGeocodingResult(address.latitude, address.longitude, address.displayName) + router.popCurrentController() + } + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + super.onCreateOptionsMenu(menu, inflater) + inflater.inflate(R.menu.menu_geocoding, menu) + searchItem = menu.findItem(R.id.geocoding_action_search) + initSearchView() + + searchItem?.expandActionView() + searchView?.setQuery(query, false) + searchView?.clearFocus() + } + + override fun onQueryTextSubmit(query: String?): Boolean { + this.query = query + searchLocation() + searchView?.clearFocus() + return true + } + + override fun onQueryTextChange(newText: String?): Boolean { + return true + } + + private fun initSearchView() { + if (activity != null) { + val searchManager = activity!!.getSystemService(Context.SEARCH_SERVICE) as SearchManager + if (searchItem != null) { + searchView = MenuItemCompat.getActionView(searchItem) as SearchView + searchView?.maxWidth = Int.MAX_VALUE + searchView?.inputType = InputType.TYPE_TEXT_VARIATION_FILTER + var imeOptions = EditorInfo.IME_ACTION_DONE or EditorInfo.IME_FLAG_NO_FULLSCREEN + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && appPreferences!!.isKeyboardIncognito) { + imeOptions = imeOptions or EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING + } + searchView?.imeOptions = imeOptions + searchView?.queryHint = resources!!.getString(R.string.nc_search) + searchView?.setSearchableInfo(searchManager.getSearchableInfo(activity!!.componentName)) + searchView?.setOnQueryTextListener(this) + + searchItem?.setOnActionExpandListener(object : MenuItem.OnActionExpandListener { + override fun onMenuItemActionExpand(menuItem: MenuItem): Boolean { + return true + } + + override fun onMenuItemActionCollapse(menuItem: MenuItem): Boolean { + router.popCurrentController() + return true + } + }) + } + } + } + + private fun initGeocoder() { + val baseUrl = context!!.getString(R.string.osm_geocoder_url) + val email = context!!.getString(R.string.osm_geocoder_contact) + nominatimClient = TalkJsonNominatimClient(baseUrl, okHttpClient, email) + } + + private fun searchLocation(): Boolean { + CoroutineScope(IO).launch { + executeGeocodingRequest() + } + return true + } + + @Suppress("Detekt.TooGenericExceptionCaught") + private suspend fun executeGeocodingRequest() { + var results: ArrayList
= ArrayList() + try { + results = nominatimClient!!.search(query) as ArrayList
+ for (address in results) { + Log.d(TAG, address.displayName) + Log.d(TAG, address.latitude.toString()) + Log.d(TAG, address.longitude.toString()) + } + } catch (e: Exception) { + Log.e(TAG, "Failed to get geocoded addresses", e) + Toast.makeText(context, R.string.nc_common_error_sorry, Toast.LENGTH_LONG).show() + } + updateResultsOnMainThread(results) + } + + private suspend fun updateResultsOnMainThread(results: ArrayList
) { + withContext(Main) { + initAdapter(results) + } + } + + interface GeocodingResultListener { + fun receiveChosenGeocodingResult(lat: Double, lon: Double, name: String) + } + + companion object { + private const val TAG = "GeocodingController" + } +} diff --git a/app/src/main/java/com/nextcloud/talk/controllers/LocationPickerController.kt b/app/src/main/java/com/nextcloud/talk/controllers/LocationPickerController.kt new file mode 100644 index 000000000..ade4d75a0 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/controllers/LocationPickerController.kt @@ -0,0 +1,492 @@ +/* + * Nextcloud Talk application + * + * @author Marcel Hibbe + * Copyright (C) 2021 Marcel Hibbe + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.controllers + +import android.Manifest +import android.app.SearchManager +import android.content.Context +import android.content.pm.PackageManager +import android.graphics.drawable.ColorDrawable +import android.location.Location +import android.location.LocationListener +import android.location.LocationManager +import android.os.Build +import android.os.Bundle +import android.text.InputType +import android.util.Log +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.inputmethod.EditorInfo +import android.widget.Toast +import androidx.appcompat.widget.SearchView +import androidx.core.content.PermissionChecker +import androidx.core.content.res.ResourcesCompat +import androidx.core.view.MenuItemCompat +import androidx.preference.PreferenceManager +import autodagger.AutoInjector +import com.bluelinelabs.conductor.RouterTransaction +import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler +import com.nextcloud.talk.R +import com.nextcloud.talk.api.NcApi +import com.nextcloud.talk.application.NextcloudTalkApplication +import com.nextcloud.talk.controllers.base.NewBaseController +import com.nextcloud.talk.controllers.util.viewBinding +import com.nextcloud.talk.databinding.ControllerLocationBinding +import com.nextcloud.talk.models.json.generic.GenericOverall +import com.nextcloud.talk.utils.ApiUtils +import com.nextcloud.talk.utils.DisplayUtils +import com.nextcloud.talk.utils.bundle.BundleKeys +import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN +import com.nextcloud.talk.utils.database.user.UserUtils +import fr.dudie.nominatim.client.TalkJsonNominatimClient +import fr.dudie.nominatim.model.Address +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import org.osmdroid.config.Configuration.getInstance +import org.osmdroid.events.DelayedMapListener +import org.osmdroid.events.MapListener +import org.osmdroid.events.ScrollEvent +import org.osmdroid.events.ZoomEvent +import org.osmdroid.tileprovider.tilesource.TileSourceFactory +import org.osmdroid.util.GeoPoint +import org.osmdroid.views.overlay.CopyrightOverlay +import org.osmdroid.views.overlay.mylocation.GpsMyLocationProvider +import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay +import javax.inject.Inject + +@AutoInjector(NextcloudTalkApplication::class) +class LocationPickerController(args: Bundle) : + NewBaseController( + R.layout.controller_location, + args + ), + SearchView.OnQueryTextListener, + LocationListener, + GeocodingController.GeocodingResultListener { + private val binding: ControllerLocationBinding by viewBinding(ControllerLocationBinding::bind) + + @Inject + lateinit var ncApi: NcApi + + @Inject + lateinit var userUtils: UserUtils + + @Inject + lateinit var okHttpClient: OkHttpClient + + var nominatimClient: TalkJsonNominatimClient? = null + + var roomToken: String? + + var myLocation: GeoPoint = GeoPoint(0.0, 0.0) + private var locationManager: LocationManager? = null + private lateinit var locationOverlay: MyLocationNewOverlay + + var moveToCurrentLocationWasClicked: Boolean = true + var readyToShareLocation: Boolean = false + var searchItem: MenuItem? = null + var searchView: SearchView? = null + + var receivedChosenGeocodingResult: Boolean = false + var geocodedLat: Double = 0.0 + var geocodedLon: Double = 0.0 + var geocodedName: String = "" + + init { + setHasOptionsMenu(true) + NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this) + getInstance().load(context, PreferenceManager.getDefaultSharedPreferences(context)) + + roomToken = args.getString(KEY_ROOM_TOKEN) + } + + override fun onAttach(view: View) { + super.onAttach(view) + initMap() + } + + @Suppress("Detekt.TooGenericExceptionCaught") + override fun onDetach(view: View) { + super.onDetach(view) + try { + locationManager!!.removeUpdates(this) + } catch (e: Exception) { + Log.e(TAG, "error when trying to remove updates for location Manager", e) + } + locationOverlay.disableMyLocation() + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + super.onCreateOptionsMenu(menu, inflater) + inflater.inflate(R.menu.menu_locationpicker, menu) + searchItem = menu.findItem(R.id.location_action_search) + initSearchView() + } + + override fun onPrepareOptionsMenu(menu: Menu) { + super.onPrepareOptionsMenu(menu) + actionBar?.setIcon(ColorDrawable(resources!!.getColor(android.R.color.transparent))) + actionBar?.title = context!!.getString(R.string.nc_share_location) + } + + override val title: String + get() = + resources!!.getString(R.string.nc_share_location) + + override fun onViewBound(view: View) { + setLocationDescription(false, receivedChosenGeocodingResult) + binding.shareLocation.isClickable = false + binding.shareLocation.setOnClickListener { + if (readyToShareLocation) { + shareLocation( + binding.map.mapCenter?.latitude, + binding.map.mapCenter?.longitude, + binding.placeName.text.toString() + ) + } else { + Log.w(TAG, "readyToShareLocation was false while user tried to share location.") + } + } + } + + private fun initSearchView() { + if (activity != null) { + val searchManager = activity!!.getSystemService(Context.SEARCH_SERVICE) as SearchManager + if (searchItem != null) { + searchView = MenuItemCompat.getActionView(searchItem) as SearchView + searchView?.maxWidth = Int.MAX_VALUE + searchView?.inputType = InputType.TYPE_TEXT_VARIATION_FILTER + var imeOptions = EditorInfo.IME_ACTION_DONE or EditorInfo.IME_FLAG_NO_FULLSCREEN + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && appPreferences!!.isKeyboardIncognito) { + imeOptions = imeOptions or EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING + } + searchView?.imeOptions = imeOptions + searchView?.queryHint = resources!!.getString(R.string.nc_search) + searchView?.setSearchableInfo(searchManager.getSearchableInfo(activity!!.componentName)) + searchView?.setOnQueryTextListener(this) + } + } + } + + override fun onQueryTextSubmit(query: String?): Boolean { + if (!query.isNullOrEmpty()) { + val bundle = Bundle() + bundle.putString(BundleKeys.KEY_GEOCODING_QUERY, query) + bundle.putString(BundleKeys.KEY_ROOM_TOKEN, roomToken) + router.pushController( + RouterTransaction.with(GeocodingController(bundle, this)) + .pushChangeHandler(HorizontalChangeHandler()) + .popChangeHandler(HorizontalChangeHandler()) + ) + } + return true + } + + override fun onQueryTextChange(newText: String?): Boolean { + return true + } + + private fun initMap() { + if (!isLocationPermissionsGranted()) { + requestLocationPermissions() + } + + binding.map.setTileSource(TileSourceFactory.MAPNIK) + + binding.map.onResume() + + locationManager = activity!!.getSystemService(Context.LOCATION_SERVICE) as LocationManager + try { + locationManager!!.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0L, 0f, this) + } catch (ex: SecurityException) { + Log.w(TAG, "Error requesting location updates", ex) + } + + val copyrightOverlay = CopyrightOverlay(context) + binding.map.overlays?.add(copyrightOverlay) + + binding.map.setMultiTouchControls(true) + binding.map.isTilesScaledToDpi = true + + locationOverlay = MyLocationNewOverlay(GpsMyLocationProvider(context), binding.map) + locationOverlay.enableMyLocation() + locationOverlay.setPersonHotspot(PERSON_HOT_SPOT_X, PERSON_HOT_SPOT_Y) + locationOverlay.setPersonIcon( + DisplayUtils.getBitmap( + ResourcesCompat.getDrawable(resources!!, R.drawable.current_location_circle, null) + ) + ) + binding.map.overlays?.add(locationOverlay) + + val mapController = binding.map.controller + + if (receivedChosenGeocodingResult) { + mapController?.setZoom(ZOOM_LEVEL_RECEIVED_RESULT) + } else { + mapController?.setZoom(ZOOM_LEVEL_DEFAULT) + } + + val zoomToCurrentPositionOnFirstFix = !receivedChosenGeocodingResult + locationOverlay.runOnFirstFix { + myLocation = locationOverlay.myLocation + if (zoomToCurrentPositionOnFirstFix) { + activity!!.runOnUiThread { + mapController?.setZoom(ZOOM_LEVEL_DEFAULT) + mapController?.setCenter(myLocation) + } + } + } + + if (receivedChosenGeocodingResult && geocodedLat != GEOCODE_ZERO && geocodedLon != GEOCODE_ZERO) { + mapController?.setCenter(GeoPoint(geocodedLat, geocodedLon)) + } + + binding.centerMapButton.setOnClickListener { + mapController?.animateTo(myLocation) + moveToCurrentLocationWasClicked = true + } + + binding.map.addMapListener( + DelayedMapListener( + object : MapListener { + @Suppress("Detekt.TooGenericExceptionCaught") + override fun onScroll(paramScrollEvent: ScrollEvent): Boolean { + try { + when { + moveToCurrentLocationWasClicked -> { + setLocationDescription(isGpsLocation = true, isGeocodedResult = false) + moveToCurrentLocationWasClicked = false + } + receivedChosenGeocodingResult -> { + binding.shareLocation.isClickable = true + setLocationDescription(isGpsLocation = false, isGeocodedResult = true) + receivedChosenGeocodingResult = false + } + else -> { + binding.shareLocation.isClickable = true + setLocationDescription(isGpsLocation = false, isGeocodedResult = false) + } + } + } catch (e: NullPointerException) { + Log.d(TAG, "UI already closed") + } + + readyToShareLocation = true + return true + } + + override fun onZoom(event: ZoomEvent): Boolean { + return false + } + }) + ) + } + + private fun setLocationDescription(isGpsLocation: Boolean, isGeocodedResult: Boolean) { + when { + isGpsLocation -> { + binding.shareLocationDescription.text = context!!.getText(R.string.nc_share_current_location) + binding.placeName.visibility = View.GONE + binding.placeName.text = "" + } + isGeocodedResult -> { + binding.shareLocationDescription.text = context!!.getText(R.string.nc_share_this_location) + binding.placeName.visibility = View.VISIBLE + binding.placeName.text = geocodedName + } + else -> { + binding.shareLocationDescription.text = context!!.getText(R.string.nc_share_this_location) + binding.placeName.visibility = View.GONE + binding.placeName.text = "" + } + } + } + + private fun shareLocation(selectedLat: Double?, selectedLon: Double?, locationName: String?) { + if (selectedLat != null || selectedLon != null) { + + val name = locationName + if (name.isNullOrEmpty()) { + initGeocoder() + searchPlaceNameForCoordinates(selectedLat!!, selectedLon!!) + } else { + executeShareLocation(selectedLat, selectedLon, locationName) + } + } + } + + private fun executeShareLocation(selectedLat: Double?, selectedLon: Double?, locationName: String?) { + val objectId = "geo:$selectedLat,$selectedLon" + val metaData: String = + "{\"type\":\"geo-location\",\"id\":\"geo:$selectedLat,$selectedLon\",\"latitude\":\"$selectedLat\"," + + "\"longitude\":\"$selectedLon\",\"name\":\"$locationName\"}" + + ncApi.sendLocation( + ApiUtils.getCredentials(userUtils.currentUser?.username, userUtils.currentUser?.token), + ApiUtils.getUrlToSendLocation(userUtils.currentUser?.baseUrl, roomToken), + "geo-location", + objectId, + metaData + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(t: GenericOverall) { + router.popCurrentController() + } + + override fun onError(e: Throwable) { + Log.e(TAG, "error when trying to share location", e) + Toast.makeText(context, R.string.nc_common_error_sorry, Toast.LENGTH_LONG).show() + router.popCurrentController() + } + + override fun onComplete() { + // unused atm + } + }) + } + + private fun isLocationPermissionsGranted(): Boolean { + fun isCoarseLocationGranted(): Boolean { + return PermissionChecker.checkSelfPermission( + context!!, + Manifest.permission.ACCESS_COARSE_LOCATION + ) == PermissionChecker.PERMISSION_GRANTED + } + + fun isFineLocationGranted(): Boolean { + return PermissionChecker.checkSelfPermission( + context!!, + Manifest.permission.ACCESS_FINE_LOCATION + ) == PermissionChecker.PERMISSION_GRANTED + } + + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + isCoarseLocationGranted() && isFineLocationGranted() + } else { + true + } + } + + private fun requestLocationPermissions() { + requestPermissions( + arrayOf( + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION + ), + REQUEST_PERMISSIONS_REQUEST_CODE + ) + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + if (requestCode == REQUEST_PERMISSIONS_REQUEST_CODE && + grantResults.size > 0 && + grantResults[0] == PackageManager.PERMISSION_GRANTED + ) { + initMap() + } else { + Toast.makeText(context, context!!.getString(R.string.nc_location_permission_required), Toast.LENGTH_LONG) + .show() + } + } + + override fun receiveChosenGeocodingResult(lat: Double, lon: Double, name: String) { + receivedChosenGeocodingResult = true + geocodedLat = lat + geocodedLon = lon + geocodedName = name + } + + private fun initGeocoder() { + val baseUrl = context!!.getString(R.string.osm_geocoder_url) + val email = context!!.getString(R.string.osm_geocoder_contact) + nominatimClient = TalkJsonNominatimClient(baseUrl, okHttpClient, email) + } + + private fun searchPlaceNameForCoordinates(lat: Double, lon: Double): Boolean { + CoroutineScope(Dispatchers.IO).launch { + executeGeocodingRequest(lat, lon) + } + return true + } + + @Suppress("Detekt.TooGenericExceptionCaught") + private suspend fun executeGeocodingRequest(lat: Double, lon: Double) { + var address: Address? = null + try { + address = nominatimClient!!.getAddress(lon, lat) + } catch (e: Exception) { + Log.e(TAG, "Failed to get geocoded addresses", e) + Toast.makeText(context, R.string.nc_common_error_sorry, Toast.LENGTH_LONG).show() + } + updateResultOnMainThread(lat, lon, address?.displayName) + } + + private suspend fun updateResultOnMainThread(lat: Double, lon: Double, addressName: String?) { + withContext(Dispatchers.Main) { + executeShareLocation(lat, lon, addressName) + } + } + + override fun onLocationChanged(location: Location?) { + myLocation = GeoPoint(location) + } + + override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) { + // empty + } + + override fun onProviderEnabled(provider: String?) { + // empty + } + + override fun onProviderDisabled(provider: String?) { + // empty + } + + companion object { + private const val TAG = "LocPicker" + private const val REQUEST_PERMISSIONS_REQUEST_CODE = 1 + private const val PERSON_HOT_SPOT_X: Float = 20.0F + private const val PERSON_HOT_SPOT_Y: Float = 20.0F + private const val ZOOM_LEVEL_RECEIVED_RESULT: Double = 14.0 + private const val ZOOM_LEVEL_DEFAULT: Double = 14.0 + private const val GEOCODE_ZERO: Double = 0.0 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/interfaces/ExtendedIMessage.kt b/app/src/main/java/com/nextcloud/talk/interfaces/ExtendedIMessage.kt new file mode 100644 index 000000000..a494eeded --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/interfaces/ExtendedIMessage.kt @@ -0,0 +1,28 @@ +/* + * Nextcloud Talk application + * + * @author Marcel Hibbe + * Copyright (C) 2021 Marcel Hibbe + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.nextcloud.talk.interfaces + +import com.stfalcon.chatkit.commons.models.IMessage + +interface ExtendedIMessage : IMessage { + + fun isLocationMessage(): Boolean +} diff --git a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.java b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.java index aa704cb35..a328fdf53 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.java +++ b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.java @@ -26,27 +26,30 @@ import com.bluelinelabs.logansquare.annotation.JsonIgnore; import com.bluelinelabs.logansquare.annotation.JsonObject; import com.nextcloud.talk.R; import com.nextcloud.talk.application.NextcloudTalkApplication; +import com.nextcloud.talk.interfaces.ExtendedIMessage; import com.nextcloud.talk.models.database.UserEntity; import com.nextcloud.talk.models.json.converters.EnumSystemMessageTypeConverter; import com.nextcloud.talk.utils.ApiUtils; import com.nextcloud.talk.utils.TextMatchers; -import com.stfalcon.chatkit.commons.models.IMessage; import com.stfalcon.chatkit.commons.models.IUser; import com.stfalcon.chatkit.commons.models.MessageContentType; import org.parceler.Parcel; +import java.security.MessageDigest; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import androidx.annotation.Nullable; +import kotlin.text.Charsets; @Parcel @JsonObject -public class ChatMessage implements IMessage, MessageContentType, MessageContentType.Image { +public class ChatMessage implements ExtendedIMessage, MessageContentType, MessageContentType.Image { @JsonIgnore public boolean isGrouped; @JsonIgnore @@ -87,15 +90,21 @@ public class ChatMessage implements IMessage, MessageContentType, MessageContent public Enum readStatus = ReadStatus.NONE; @JsonIgnore - List messageTypesToIgnore = Arrays.asList(MessageType.REGULAR_TEXT_MESSAGE, - MessageType.SYSTEM_MESSAGE, MessageType.SINGLE_LINK_VIDEO_MESSAGE, - MessageType.SINGLE_LINK_AUDIO_MESSAGE, MessageType.SINGLE_LINK_MESSAGE); + List messageTypesToIgnore = Arrays.asList( + MessageType.REGULAR_TEXT_MESSAGE, + MessageType.SYSTEM_MESSAGE, + MessageType.SINGLE_LINK_VIDEO_MESSAGE, + MessageType.SINGLE_LINK_AUDIO_MESSAGE, + MessageType.SINGLE_LINK_MESSAGE, + MessageType.SINGLE_NC_GEOLOCATION_MESSAGE); public boolean hasFileAttachment() { if (messageParameters != null && messageParameters.size() > 0) { - for (String key : messageParameters.keySet()) { - Map individualHashMap = messageParameters.get(key); - if (individualHashMap.get("type").equals("file")) { + for (HashMap.Entry> entry : messageParameters.entrySet()) { + Map individualHashMap = entry.getValue(); + if(MessageDigest.isEqual( + Objects.requireNonNull(individualHashMap.get("type")).getBytes(Charsets.UTF_8), + ("file").getBytes(Charsets.UTF_8))) { return true; } } @@ -103,13 +112,31 @@ public class ChatMessage implements IMessage, MessageContentType, MessageContent return false; } + private boolean hasGeoLocation() { + if (messageParameters != null && messageParameters.size() > 0) { + for (HashMap.Entry> entry : messageParameters.entrySet()) { + Map individualHashMap = entry.getValue(); + + if(MessageDigest.isEqual( + Objects.requireNonNull(individualHashMap.get("type")).getBytes(Charsets.UTF_8), + ("geo-location").getBytes(Charsets.UTF_8))) { + return true; + } + } + } + + return false; + } + @Nullable @Override public String getImageUrl() { if (messageParameters != null && messageParameters.size() > 0) { - for (String key : messageParameters.keySet()) { - Map individualHashMap = messageParameters.get(key); - if (individualHashMap.get("type").equals("file")) { + for (HashMap.Entry> entry : messageParameters.entrySet()) { + Map individualHashMap = entry.getValue(); + if(MessageDigest.isEqual( + Objects.requireNonNull(individualHashMap.get("type")).getBytes(Charsets.UTF_8), + ("file").getBytes(Charsets.UTF_8))) { selectedIndividualHashMap = individualHashMap; return (ApiUtils.getUrlForFilePreviewWithFileId(getActiveUser().getBaseUrl(), individualHashMap.get("id"), NextcloudTalkApplication.Companion.getSharedApplication().getResources().getDimensionPixelSize(R.dimen.maximum_file_preview_size))); @@ -133,6 +160,11 @@ public class ChatMessage implements IMessage, MessageContentType, MessageContent return MessageType.SINGLE_NC_ATTACHMENT_MESSAGE; } + if (hasGeoLocation()) { + return MessageType.SINGLE_NC_GEOLOCATION_MESSAGE; + } + + return TextMatchers.getMessageTypeFromString(getText()); } @@ -158,22 +190,29 @@ public class ChatMessage implements IMessage, MessageContentType, MessageContent if (getMessageType().equals(MessageType.REGULAR_TEXT_MESSAGE) || getMessageType().equals(MessageType.SYSTEM_MESSAGE) || getMessageType().equals(MessageType.SINGLE_LINK_MESSAGE)) { return getText(); } else { - if (getMessageType().equals(MessageType.SINGLE_LINK_GIPHY_MESSAGE) - || getMessageType().equals(MessageType.SINGLE_LINK_TENOR_MESSAGE) - || getMessageType().equals(MessageType.SINGLE_LINK_GIF_MESSAGE)) { + if (MessageType.SINGLE_LINK_GIPHY_MESSAGE == getMessageType() + || MessageType.SINGLE_LINK_TENOR_MESSAGE == getMessageType() + || MessageType.SINGLE_LINK_GIF_MESSAGE == getMessageType()) { if (getActorId().equals(getActiveUser().getUserId())) { return (NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_sent_a_gif_you)); } else { return (String.format(NextcloudTalkApplication.Companion.getSharedApplication().getResources().getString(R.string.nc_sent_a_gif), !TextUtils.isEmpty(getActorDisplayName()) ? getActorDisplayName() : NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest))); } - } else if (getMessageType().equals(MessageType.SINGLE_NC_ATTACHMENT_MESSAGE)) { + } else if (MessageType.SINGLE_NC_ATTACHMENT_MESSAGE == getMessageType()) { if (getActorId().equals(getActiveUser().getUserId())) { return (NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_sent_an_attachment_you)); } else { return (String.format(NextcloudTalkApplication.Companion.getSharedApplication().getResources().getString(R.string.nc_sent_an_attachment), !TextUtils.isEmpty(getActorDisplayName()) ? getActorDisplayName() : NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest))); } + } else if (MessageType.SINGLE_NC_GEOLOCATION_MESSAGE == getMessageType()) { + if (getActorId().equals(getActiveUser().getUserId())) { + return (NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_sent_location_you)); + } else { + return (String.format(NextcloudTalkApplication.Companion.getSharedApplication().getResources().getString(R.string.nc_sent_location), + !TextUtils.isEmpty(getActorDisplayName()) ? getActorDisplayName() : NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest))); + } /*} else if (getMessageType().equals(MessageType.SINGLE_LINK_MESSAGE)) { if (getActorId().equals(getActiveUser().getUserId())) { return (NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_sent_a_link_you)); @@ -181,21 +220,21 @@ public class ChatMessage implements IMessage, MessageContentType, MessageContent return (String.format(NextcloudTalkApplication.Companion.getSharedApplication().getResources().getString(R.string.nc_sent_a_link), !TextUtils.isEmpty(getActorDisplayName()) ? getActorDisplayName() : NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest))); }*/ - } else if (getMessageType().equals(MessageType.SINGLE_LINK_AUDIO_MESSAGE)) { + } else if (MessageType.SINGLE_LINK_AUDIO_MESSAGE == getMessageType()) { if (getActorId().equals(getActiveUser().getUserId())) { return (NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_sent_an_audio_you)); } else { return (String.format(NextcloudTalkApplication.Companion.getSharedApplication().getResources().getString(R.string.nc_sent_an_audio), !TextUtils.isEmpty(getActorDisplayName()) ? getActorDisplayName() : NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest))); } - } else if (getMessageType().equals(MessageType.SINGLE_LINK_VIDEO_MESSAGE)) { + } else if (MessageType.SINGLE_LINK_VIDEO_MESSAGE == getMessageType()) { if (getActorId().equals(getActiveUser().getUserId())) { return (NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_sent_a_video_you)); } else { return (String.format(NextcloudTalkApplication.Companion.getSharedApplication().getResources().getString(R.string.nc_sent_a_video), !TextUtils.isEmpty(getActorDisplayName()) ? getActorDisplayName() : NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_guest))); } - } else if (getMessageType().equals(MessageType.SINGLE_LINK_IMAGE_MESSAGE)) { + } else if (MessageType.SINGLE_LINK_IMAGE_MESSAGE == getMessageType()) { if (getActorId().equals(getActiveUser().getUserId())) { return (NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_sent_an_image_you)); } else { @@ -537,6 +576,11 @@ public class ChatMessage implements IMessage, MessageContentType, MessageContent return "ChatMessage(isGrouped=" + this.isGrouped() + ", isOneToOneConversation=" + this.isOneToOneConversation() + ", activeUser=" + this.getActiveUser() + ", selectedIndividualHashMap=" + this.getSelectedIndividualHashMap() + ", isLinkPreviewAllowed=" + this.isLinkPreviewAllowed() + ", isDeleted=" + this.isDeleted() + ", jsonMessageId=" + this.getJsonMessageId() + ", token=" + this.getToken() + ", actorType=" + this.getActorType() + ", actorId=" + this.getActorId() + ", actorDisplayName=" + this.getActorDisplayName() + ", timestamp=" + this.getTimestamp() + ", message=" + this.getMessage() + ", messageParameters=" + this.getMessageParameters() + ", systemMessageType=" + this.getSystemMessageType() + ", replyable=" + this.isReplyable() + ", parentMessage=" + this.getParentMessage() + ", readStatus=" + this.getReadStatus() + ", messageTypesToIgnore=" + this.getMessageTypesToIgnore() + ")"; } + @Override + public boolean isLocationMessage() { + return hasGeoLocation(); + } + public enum MessageType { REGULAR_TEXT_MESSAGE, SYSTEM_MESSAGE, @@ -548,6 +592,7 @@ public class ChatMessage implements IMessage, MessageContentType, MessageContent SINGLE_LINK_IMAGE_MESSAGE, SINGLE_LINK_AUDIO_MESSAGE, SINGLE_NC_ATTACHMENT_MESSAGE, + SINGLE_NC_GEOLOCATION_MESSAGE, } public enum SystemMessageType { diff --git a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatUtils.kt b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatUtils.kt index 9e060409f..984a858ef 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/chat/ChatUtils.kt @@ -33,6 +33,8 @@ class ChatUtils { val type = individualHashMap?.get("type") if (type == "user" || type == "guest" || type == "call") { resultMessage = resultMessage?.replace("{$key}", "@" + individualHashMap["name"]) + } else if (type == "geo-location") { + resultMessage = individualHashMap.get("name") } else if (individualHashMap?.containsKey("link") == true) { resultMessage = if (type == "file") { resultMessage?.replace("{$key}", individualHashMap["name"].toString()) diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt index 29e47b131..89b4b88a7 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt @@ -2,7 +2,7 @@ * Nextcloud Talk application * * @author Marcel Hibbe - * Copyright (C) 2021 Marcel Hibbe + * Copyright (C) 2021 Marcel Hibbe * * 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 @@ -22,7 +22,9 @@ package com.nextcloud.talk.ui.dialog import android.app.Activity import android.os.Bundle +import android.view.View import android.view.ViewGroup +import android.widget.LinearLayout import androidx.appcompat.widget.AppCompatTextView import butterknife.BindView import butterknife.ButterKnife @@ -35,6 +37,14 @@ import com.nextcloud.talk.models.database.CapabilitiesUtil class AttachmentDialog(val activity: Activity, var chatController: ChatController) : BottomSheetDialog(activity) { + @BindView(R.id.menu_share_location) + @JvmField + var shareLocationItem: LinearLayout? = null + + @BindView(R.id.txt_share_location) + @JvmField + var shareLocation: AppCompatTextView? = null + @BindView(R.id.txt_attach_file_from_local) @JvmField var attachFromLocal: AppCompatTextView? = null @@ -60,6 +70,19 @@ class AttachmentDialog(val activity: Activity, var chatController: ChatControlle String.format(it.getString(R.string.nc_upload_from_cloud), serverName) } + if (!CapabilitiesUtil.hasSpreedFeatureCapability( + chatController.conversationUser, + "geo-location-sharing" + ) + ) { + shareLocationItem?.visibility = View.GONE + } + + shareLocation?.setOnClickListener { + chatController.showShareLocationScreen() + dismiss() + } + attachFromLocal?.setOnClickListener { chatController.sendSelectLocalFileIntent() dismiss() diff --git a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java index ebb129776..218b68069 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java +++ b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java @@ -389,4 +389,8 @@ public class ApiUtils { public static String getUrlForUserFields(String baseUrl) { return baseUrl + ocsApiVersion + "/cloud/user/fields"; } + + public static String getUrlToSendLocation(String baseUrl, String roomToken) { + return baseUrl + ocsApiVersion + "/apps/spreed/api/v1/chat/" + roomToken + "/share"; + } } diff --git a/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java b/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java index 397c084ea..48408670a 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java +++ b/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java @@ -55,19 +55,6 @@ import android.view.Window; import android.widget.EditText; import android.widget.TextView; -import androidx.annotation.ColorInt; -import androidx.annotation.ColorRes; -import androidx.annotation.DrawableRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.XmlRes; -import androidx.appcompat.widget.AppCompatDrawableManager; -import androidx.appcompat.widget.SearchView; -import androidx.core.content.ContextCompat; -import androidx.core.graphics.ColorUtils; -import androidx.core.graphics.drawable.DrawableCompat; -import androidx.emoji.text.EmojiCompat; - import com.facebook.common.executors.UiThreadImmediateExecutorService; import com.facebook.common.references.CloseableReference; import com.facebook.datasource.DataSource; @@ -102,6 +89,19 @@ import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; +import androidx.annotation.ColorInt; +import androidx.annotation.ColorRes; +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.XmlRes; +import androidx.appcompat.widget.AppCompatDrawableManager; +import androidx.appcompat.widget.SearchView; +import androidx.core.content.ContextCompat; +import androidx.core.graphics.ColorUtils; +import androidx.core.graphics.drawable.DrawableCompat; +import androidx.emoji.text.EmojiCompat; + public class DisplayUtils { private static final String TAG = "DisplayUtils"; @@ -160,7 +160,7 @@ public class DisplayUtils { return new BitmapDrawable(getRoundedBitmapFromVectorDrawableResource(resources, resource)); } - private static Bitmap getBitmap(Drawable drawable) { + public static Bitmap getBitmap(Drawable drawable) { Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); diff --git a/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt b/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt index 39ea5d998..bc66d2691 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/bundle/BundleKeys.kt @@ -66,4 +66,5 @@ object BundleKeys { val KEY_FILE_ID = "KEY_FILE_ID" val KEY_NOTIFICATION_ID = "KEY_NOTIFICATION_ID" val KEY_SHARED_TEXT = "KEY_SHARED_TEXT" + val KEY_GEOCODING_QUERY = "KEY_GEOCODING_QUERY" } diff --git a/app/src/main/java/fr/dudie/nominatim/client/TalkJsonNominatimClient.java b/app/src/main/java/fr/dudie/nominatim/client/TalkJsonNominatimClient.java new file mode 100644 index 000000000..380a03244 --- /dev/null +++ b/app/src/main/java/fr/dudie/nominatim/client/TalkJsonNominatimClient.java @@ -0,0 +1,312 @@ +package fr.dudie.nominatim.client; + +/* + * [license] + * Nominatim Java API client + * ~~~~ + * Copyright (C) 2010 - 2014 Dudie + * ~~~~ + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser 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 Lesser Public License for more details. + * + * You should have received a copy of the GNU General Lesser Public + * License along with this program. If not, see + * . + * [/license] + */ + +import android.util.Log; + +import com.github.filosganga.geogson.gson.GeometryAdapterFactory; +import com.github.filosganga.geogson.jts.JtsAdapterFactory; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.List; + +import fr.dudie.nominatim.client.request.NominatimLookupRequest; +import fr.dudie.nominatim.client.request.NominatimReverseRequest; +import fr.dudie.nominatim.client.request.NominatimSearchRequest; +import fr.dudie.nominatim.client.request.paramhelper.OsmType; +import fr.dudie.nominatim.gson.ArrayOfAddressElementsDeserializer; +import fr.dudie.nominatim.gson.ArrayOfPolygonPointsDeserializer; +import fr.dudie.nominatim.gson.BoundingBoxDeserializer; +import fr.dudie.nominatim.gson.PolygonPointDeserializer; +import fr.dudie.nominatim.model.Address; +import fr.dudie.nominatim.model.BoundingBox; +import fr.dudie.nominatim.model.Element; +import fr.dudie.nominatim.model.PolygonPoint; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; + +/** + * An implementation of the Nominatim Api Service. + * + * @author Jérémie Huchet + * @author Sunil D S + * @author Andy Scherzinger + */ +public final class TalkJsonNominatimClient implements NominatimClient { + private static final String TAG = "TalkNominationClient"; + + /** + * UTF-8 encoding. + */ + public static final String ENCODING_UTF_8 = "UTF-8"; + + private final OkHttpClient httpClient; + + /** + * Gson instance for Nominatim API calls. + */ + private final Gson gson; + + /** + * The url to make search queries. + */ + private final String searchUrl; + + /** + * The url for reverse geocoding. + */ + private final String reverseUrl; + + /** + * The url for address lookup. + */ + private final String lookupUrl; + + /** + * The default search options. + */ + private final NominatimOptions defaults; + + /** + * Creates the json nominatim client. + * + * @param baseUrl the nominatim server url + * @param httpClient an HTTP client + * @param email an email to add in the HTTP requests parameters to "sign" them (see + * https://wiki.openstreetmap.org/wiki/Nominatim_usage_policy) + */ + public TalkJsonNominatimClient(final String baseUrl, final OkHttpClient httpClient, final String email) { + this(baseUrl, httpClient, email, new NominatimOptions()); + } + + /** + * Creates the json nominatim client. + * + * @param baseUrl the nominatim server url + * @param httpClient an HTTP client + * @param email an email to add in the HTTP requests parameters to "sign" them (see + * https://wiki.openstreetmap.org/wiki/Nominatim_usage_policy) + * @param defaults defaults options, they override null valued requests options + */ + public TalkJsonNominatimClient(final String baseUrl, final OkHttpClient httpClient, final String email, final NominatimOptions defaults) { + String emailEncoded; + try { + emailEncoded = URLEncoder.encode(email, ENCODING_UTF_8); + } catch (UnsupportedEncodingException e) { + emailEncoded = email; + } + this.searchUrl = String.format("%s/search?format=jsonv2&email=%s", baseUrl.replaceAll("/$", ""), emailEncoded); + this.reverseUrl = String.format("%s/reverse?format=jsonv2&email=%s", baseUrl.replaceAll("/$", ""), emailEncoded); + this.lookupUrl = String.format("%s/lookup?format=json&email=%s", baseUrl.replaceAll("/$", ""), emailEncoded); + + Log.d(TAG, "API search URL: " + searchUrl); + Log.d(TAG, "API reverse URL: " + reverseUrl); + + this.defaults = defaults; + + // prepare gson instance + final GsonBuilder gsonBuilder = new GsonBuilder(); + + gsonBuilder.registerTypeAdapter(Element[].class, new ArrayOfAddressElementsDeserializer()); + gsonBuilder.registerTypeAdapter(PolygonPoint.class, new PolygonPointDeserializer()); + gsonBuilder.registerTypeAdapter(PolygonPoint[].class, new ArrayOfPolygonPointsDeserializer()); + gsonBuilder.registerTypeAdapter(BoundingBox.class, new BoundingBoxDeserializer()); + + gsonBuilder.registerTypeAdapterFactory(new JtsAdapterFactory()); + gsonBuilder.registerTypeAdapterFactory(new GeometryAdapterFactory()); + + gson = gsonBuilder.create(); + + // prepare httpclient + this.httpClient = httpClient; + } + + /** + * {@inheritDoc} + * + * @see fr.dudie.nominatim.client.NominatimClient#search(fr.dudie.nominatim.client.request.NominatimSearchRequest) + */ + @Override + public List
search(final NominatimSearchRequest search) throws IOException { + + defaults.mergeTo(search); + final String apiCall = String.format("%s&%s", searchUrl, search.getQueryString()); + Log.d(TAG, "search url: " + apiCall); + + Request requesthttp = new Request.Builder() + .addHeader("accept", "application/json") + .url(apiCall) + .build(); + + Response response = httpClient.newCall(requesthttp).execute(); + if (response.isSuccessful()) { + ResponseBody responseBody = response.body(); + if (responseBody != null) { + return gson.fromJson(responseBody.string(), new TypeToken>() { + }.getType()); + } + } + + return new ArrayList<>(); + } + + /** + * {@inheritDoc} + * + * @see fr.dudie.nominatim.client.NominatimClient#getAddress(fr.dudie.nominatim.client.request.NominatimReverseRequest) + */ + @Override + public Address getAddress(final NominatimReverseRequest reverse) throws IOException { + + final String apiCall = String.format("%s&%s", reverseUrl, reverse.getQueryString()); + Log.d(TAG, "reverse geocoding url: " + apiCall); + + Request requesthttp = new Request.Builder() + .addHeader("accept", "application/json") + .url(apiCall) + .build(); + + Response response = httpClient.newCall(requesthttp).execute(); + if (response.isSuccessful()) { + ResponseBody responseBody = response.body(); + if (responseBody != null) { + return gson.fromJson(responseBody.string(), Address.class); + } + } + + return null; + } + + /** + * {@inheritDoc} + * + * @see fr.dudie.nominatim.client.NominatimClient#lookupAddress(fr.dudie.nominatim.client.request.NominatimLookupRequest) + */ + @Override + public List
lookupAddress(final NominatimLookupRequest lookup) throws IOException { + + final String apiCall = String.format("%s&%s", lookupUrl, lookup.getQueryString()); + Log.d(TAG, "lookup url: " + apiCall); + Request requesthttp = new Request.Builder() + .addHeader("accept", "application/json") + .url(apiCall) + .build(); + + Response response = httpClient.newCall(requesthttp).execute(); + if (response.isSuccessful()) { + ResponseBody responseBody = response.body(); + if (responseBody != null) { + return gson.fromJson(responseBody.string(), new TypeToken>() { + }.getType()); + } + } + + return new ArrayList<>(); + } + + /** + * {@inheritDoc} + * + * @see fr.dudie.nominatim.client.NominatimClient#search(java.lang.String) + */ + @Override + public List
search(final String query) throws IOException { + + final NominatimSearchRequest q = new NominatimSearchRequest(); + q.setQuery(query); + return this.search(q); + } + + /** + * {@inheritDoc} + * + * @see fr.dudie.nominatim.client.NominatimClient#getAddress(double, double) + */ + @Override + public Address getAddress(final double longitude, final double latitude) throws IOException { + + final NominatimReverseRequest q = new NominatimReverseRequest(); + q.setQuery(longitude, latitude); + return this.getAddress(q); + } + + /** + * {@inheritDoc} + * + * @see fr.dudie.nominatim.client.NominatimClient#getAddress(double, double, int) + */ + @Override + public Address getAddress(final double longitude, final double latitude, final int zoom) + throws IOException { + + final NominatimReverseRequest q = new NominatimReverseRequest(); + q.setQuery(longitude, latitude); + q.setZoom(zoom); + return this.getAddress(q); + } + + /** + * {@inheritDoc} + * + * @see fr.dudie.nominatim.client.NominatimClient#getAddress(int, int) + */ + @Override + public Address getAddress(final int longitudeE6, final int latitudeE6) throws IOException { + + return this.getAddress((longitudeE6 / 1E6), (latitudeE6 / 1E6)); + } + + /** + * {@inheritDoc} + * + * @see fr.dudie.nominatim.client.NominatimClient#getAddress(String, long) + */ + @Override + public Address getAddress(final String type, final long id) throws IOException { + + final NominatimReverseRequest q = new NominatimReverseRequest(); + q.setQuery(OsmType.from(type), id); + return this.getAddress(q); + } + + /** + * {@inheritDoc} + * + * @see fr.dudie.nominatim.client.NominatimClient#lookupAddress(java.util.List) + */ + @Override + public List
lookupAddress(final List typeId) throws IOException { + + final NominatimLookupRequest q = new NominatimLookupRequest(); + q.setQuery(typeId); + return this.lookupAddress(q); + } +} diff --git a/app/src/main/res/drawable-night/ic_circular_location.xml b/app/src/main/res/drawable-night/ic_circular_location.xml new file mode 100644 index 000000000..d000c0bf1 --- /dev/null +++ b/app/src/main/res/drawable-night/ic_circular_location.xml @@ -0,0 +1,34 @@ + + + + + + diff --git a/app/src/main/res/drawable/current_location_circle.xml b/app/src/main/res/drawable/current_location_circle.xml new file mode 100644 index 000000000..fc090e473 --- /dev/null +++ b/app/src/main/res/drawable/current_location_circle.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_baseline_gps_fixed_24.xml b/app/src/main/res/drawable/ic_baseline_gps_fixed_24.xml new file mode 100644 index 000000000..79c4bf857 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_gps_fixed_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_location_on_24.xml b/app/src/main/res/drawable/ic_baseline_location_on_24.xml new file mode 100644 index 000000000..5c29525a4 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_location_on_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_location_on_red_24.xml b/app/src/main/res/drawable/ic_baseline_location_on_red_24.xml new file mode 100644 index 000000000..d8b0f5f0c --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_location_on_red_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_circular_location.xml b/app/src/main/res/drawable/ic_circular_location.xml new file mode 100644 index 000000000..58a22072a --- /dev/null +++ b/app/src/main/res/drawable/ic_circular_location.xml @@ -0,0 +1,34 @@ + + + + + + diff --git a/app/src/main/res/layout/controller_geocoding.xml b/app/src/main/res/layout/controller_geocoding.xml new file mode 100644 index 000000000..5630edd04 --- /dev/null +++ b/app/src/main/res/layout/controller_geocoding.xml @@ -0,0 +1,34 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/controller_location.xml b/app/src/main/res/layout/controller_location.xml new file mode 100644 index 000000000..ed6b775df --- /dev/null +++ b/app/src/main/res/layout/controller_location.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_attachment.xml b/app/src/main/res/layout/dialog_attachment.xml index 70ccc5e38..c2b170f3f 100644 --- a/app/src/main/res/layout/dialog_attachment.xml +++ b/app/src/main/res/layout/dialog_attachment.xml @@ -38,6 +38,38 @@ android:textColor="@color/medium_emphasis_text" android:textSize="@dimen/bottom_sheet_text_size" /> + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_custom_incoming_location_message.xml b/app/src/main/res/layout/item_custom_incoming_location_message.xml new file mode 100644 index 000000000..88f2275b2 --- /dev/null +++ b/app/src/main/res/layout/item_custom_incoming_location_message.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_custom_incoming_text_message.xml b/app/src/main/res/layout/item_custom_incoming_text_message.xml index 3085befdc..2c3449847 100644 --- a/app/src/main/res/layout/item_custom_incoming_text_message.xml +++ b/app/src/main/res/layout/item_custom_incoming_text_message.xml @@ -2,6 +2,8 @@ ~ Nextcloud Talk application ~ ~ @author Mario Danic + ~ @author Andy Scherzinger + ~ Copyright (C) 2021 Andy Scherzinger ~ Copyright (C) 2017-2018 Mario Danic ~ ~ This program is free software: you can redistribute it and/or modify @@ -47,7 +49,10 @@ app:flexWrap="wrap" app:justifyContent="flex_end"> - + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_custom_outcoming_text_message.xml b/app/src/main/res/layout/item_custom_outcoming_text_message.xml index b5ec1fc8c..26c8aed3f 100644 --- a/app/src/main/res/layout/item_custom_outcoming_text_message.xml +++ b/app/src/main/res/layout/item_custom_outcoming_text_message.xml @@ -41,7 +41,10 @@ app:flexWrap="wrap" app:justifyContent="flex_end"> - + + + + + + + + + diff --git a/app/src/main/res/menu/menu_locationpicker.xml b/app/src/main/res/menu/menu_locationpicker.xml new file mode 100644 index 000000000..b8753d983 --- /dev/null +++ b/app/src/main/res/menu/menu_locationpicker.xml @@ -0,0 +1,33 @@ + + + + + + + + + diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index 12b249d12..f70a55e11 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -39,6 +39,7 @@ #61ffffff #121212 + #99121212 @color/grey950 #FFFFFF diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index c1ad8456c..83dad5b08 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -67,6 +67,7 @@ #FFFFFF #FFFFFF + #99FFFFFF @color/grey950 #333333 diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 5cda48c58..8f76e5cc3 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -38,6 +38,8 @@ 6dp 8dp + 18sp + 192dp 80dp @@ -50,6 +52,7 @@ 32dp 72dp 72dp + 32dp 16dp 8dp 8dp diff --git a/app/src/main/res/values/setup.xml b/app/src/main/res/values/setup.xml index 137ae3611..2435ca1bf 100644 --- a/app/src/main/res/values/setup.xml +++ b/app/src/main/res/values/setup.xml @@ -54,4 +54,11 @@ 1:829118773643:android:54b65087c544d819 AIzaSyAWIyOcLafaFp8PFL61h64cy1NNZW2cU_s nextcloud-a7dea.appspot.com + + + https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png + OpenStreetMap contributors + https://nominatim.openstreetmap.org/ + android@nextcloud.com + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ef7af6d7e..8fc62aa8a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -275,12 +275,14 @@ %1$s sent an audio. %1$s sent a video. %1$s sent an image. + %1$s sent a location. You sent a link. You sent a GIF. You sent an attachment. You sent an audio. You sent a video. You sent an image. + You sent a location. %1$s: %2$s Cancel reply @@ -371,6 +373,13 @@ Send this file to %1$s? Uploading + + Share location + location permission is required + Share current location + Share this location + Your current location + phone_book_integration Match contacts based on phone number to integrate Talk shortcut into system contacts app diff --git a/drawable_resources/other/circular_location.svg b/drawable_resources/other/circular_location.svg new file mode 100644 index 000000000..262957d8d --- /dev/null +++ b/drawable_resources/other/circular_location.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/scripts/analysis/findbugs-results.txt b/scripts/analysis/findbugs-results.txt index 2415c0659..662d98cc9 100644 --- a/scripts/analysis/findbugs-results.txt +++ b/scripts/analysis/findbugs-results.txt @@ -1 +1 @@ -448 \ No newline at end of file +436 \ No newline at end of file diff --git a/scripts/analysis/lint-results.txt b/scripts/analysis/lint-results.txt index b0fd55a3e..96f95f5ca 100644 --- a/scripts/analysis/lint-results.txt +++ b/scripts/analysis/lint-results.txt @@ -1,2 +1,2 @@ DO NOT TOUCH; GENERATED BY DRONE - Lint Report: 3 errors and 290 warnings + Lint Report: 3 errors and 275 warnings