diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8ed18a833e4206f7b9338d4c601b9fd978b21066..31da6db528c8d48784502e0a128175bd7fea8166 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -50,7 +50,7 @@ deploy: dependencies: - "build" script: - - "echo $S3_CONFIG > $HOME/.s3cfg" + - "echo $S3_CONFIG | base64 -d > $HOME/.s3cfg" - "export VERSION=$(ls *.apk)" - "s3cmd put $VERSION s3://releases/quasseldroid-ng/$VERSION" - "s3cmd cp s3://releases/quasseldroid-ng/$VERSION s3://releases/quasseldroid-ng/Quasseldroid-latest.apk" diff --git a/README.md b/README.md index ac716adf61eb2f861ee17851692529ed7712ae20..7a01f76c2e526163cfebab25c9f5e8a5a637bffe 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,8 @@ Authors of the previous version of Quasseldroid: GPLv3 * [**Reactive Streams**](https://github.com/ReactiveX/RxJava) CC0 +* [**Retrofit**](https://square.github.io/retrofit/) + Apache-2.0 * [**RxJava**](https://github.com/ReactiveX/RxJava) Apache-2.0 * [**ThreeTen backport project**](http://www.threeten.org/threetenbp/) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ada6f42557d3598086076defc958b3568f11ad47..45bfb63576f41bb4911cae7904bd0fc9de551da8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -36,6 +36,25 @@ * with this program. If not, see <http://www.gnu.org/licenses/>. */ +/* + * Quasseldroid - Quassel client for Android + * + * Copyright (c) 2018 Janne Koschinski + * Copyright (c) 2018 The Quassel Project + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3 as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + import org.gradle.api.Project import java.io.FileInputStream import java.util.* @@ -169,6 +188,8 @@ dependencies { implementation("org.jetbrains", "annotations", "16.0.1") implementation("com.google.code.gson", "gson", "2.8.2") implementation("commons-codec", "commons-codec", "1.11") + implementation("com.squareup.retrofit2", "retrofit", "2.4.0") + implementation("com.squareup.retrofit2", "converter-gson", "2.4.0") withVersion("8.8.1") { implementation("com.jakewharton", "butterknife", version) kapt("com.jakewharton", "butterknife-compiler", version) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index a6839415a8537b7ed338f6be54601e4704cec5c7..5d4a7e48c3256ed0a4399a9a968d4887655b6311 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -41,4 +41,20 @@ # Gson -dontnote com.google.gson.** # Dagger --dontwarn com.google.errorprone.annotations.* \ No newline at end of file +-dontwarn com.google.errorprone.annotations.* +# Retrofit +# Platform calls Class.forName on types which do not exist on Android to determine platform. +-dontnote retrofit2.Platform +# Platform used when running on Java 8 VMs. Will not be used at runtime. +-dontwarn retrofit2.Platform$Java8 +# Annotation used by Retrofit on Java 8 VMs +-dontwarn javax.annotation.Nullable +-dontwarn javax.annotation.ParametersAreNonnullByDefault +-dontwarn javax.annotation.concurrent.GuardedBy +# Retain generic type information for use by reflection by converters and adapters. +-keepattributes Signature +# Retain declared checked exceptions for use by a Proxy instance. +-keepattributes Exceptions +# Okio +-dontwarn okio.** +-dontwarn org.conscrypt.** diff --git a/app/src/main/java/de/kuschku/quasseldroid/QuasseldroidGlideModule.kt b/app/src/main/java/de/kuschku/quasseldroid/QuasseldroidGlideModule.kt index ee972ab5f23e2e8743dcadec7b6b6fa095441f3d..cea18d1376edea8207511c8c20b183472233eaee 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/QuasseldroidGlideModule.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/QuasseldroidGlideModule.kt @@ -21,13 +21,44 @@ package de.kuschku.quasseldroid import android.content.Context import android.util.Log +import com.bumptech.glide.Glide import com.bumptech.glide.GlideBuilder +import com.bumptech.glide.Registry import com.bumptech.glide.annotation.GlideModule +import com.bumptech.glide.load.model.ModelLoader +import com.bumptech.glide.load.model.ModelLoaderFactory +import com.bumptech.glide.load.model.MultiModelLoaderFactory import com.bumptech.glide.module.AppGlideModule +import com.google.gson.GsonBuilder +import de.kuschku.quasseldroid.util.avatars.MatrixApi +import de.kuschku.quasseldroid.util.avatars.MatrixModelLoader +import de.kuschku.quasseldroid.viewmodel.data.Avatar +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import java.io.InputStream @GlideModule class QuasseldroidGlideModule : AppGlideModule() { override fun applyOptions(context: Context, builder: GlideBuilder) { if (!BuildConfig.DEBUG) builder.setLogLevel(Log.ERROR) } + + override fun registerComponents(context: Context, glide: Glide, registry: Registry) { + val matrixApi = Retrofit.Builder() + .baseUrl("https://matrix.org/") + .addConverterFactory(GsonConverterFactory.create(GsonBuilder().setLenient().create())) + .build() + .create(MatrixApi::class.java) + + registry.append(Avatar.MatrixAvatar::class.java, + InputStream::class.java, + object : ModelLoaderFactory<Avatar.MatrixAvatar, InputStream> { + override fun build( + multiFactory: MultiModelLoaderFactory): ModelLoader<Avatar.MatrixAvatar, InputStream> { + return MatrixModelLoader(matrixApi) + } + + override fun teardown() = Unit + }) + } } diff --git a/app/src/main/java/de/kuschku/quasseldroid/service/QuasselNotificationBackend.kt b/app/src/main/java/de/kuschku/quasseldroid/service/QuasselNotificationBackend.kt index 0c14dc7e44d3b46da742f1c893bd929f2242492b..80456bf453a568563f63c952cc2227b196d89216 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/service/QuasselNotificationBackend.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/service/QuasselNotificationBackend.kt @@ -38,8 +38,8 @@ import de.kuschku.quasseldroid.settings.AppearanceSettings import de.kuschku.quasseldroid.settings.MessageSettings import de.kuschku.quasseldroid.settings.NotificationSettings import de.kuschku.quasseldroid.settings.Settings -import de.kuschku.quasseldroid.util.AvatarHelper import de.kuschku.quasseldroid.util.NotificationMessage +import de.kuschku.quasseldroid.util.avatars.AvatarHelper import de.kuschku.quasseldroid.util.helper.getColorCompat import de.kuschku.quasseldroid.util.helper.loadWithFallbacks import de.kuschku.quasseldroid.util.helper.styledAttributes diff --git a/app/src/main/java/de/kuschku/quasseldroid/settings/MessageSettings.kt b/app/src/main/java/de/kuschku/quasseldroid/settings/MessageSettings.kt index 15aae626cb8409fd026c09302026019f3a91fffe..459dc2ea0222555fff894b1cd788c3712c20f754 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/settings/MessageSettings.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/settings/MessageSettings.kt @@ -34,6 +34,7 @@ data class MessageSettings( val showAvatars: Boolean = true, val showIRCCloudAvatars: Boolean = false, val showGravatarAvatars: Boolean = false, + val showMatrixAvatars: Boolean = false, val largerEmoji: Boolean = false ) { diff --git a/app/src/main/java/de/kuschku/quasseldroid/settings/Settings.kt b/app/src/main/java/de/kuschku/quasseldroid/settings/Settings.kt index 9a1730e64b3c743d85a0b3830a4546bb48453a78..997e99d8ee00696fb90c5104890423a3d8679ac8 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/settings/Settings.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/settings/Settings.kt @@ -109,6 +109,10 @@ object Settings { context.getString(R.string.preference_show_gravatar_avatars_key), MessageSettings.DEFAULT.showGravatarAvatars ), + showMatrixAvatars = getBoolean( + context.getString(R.string.preference_show_matrix_avatars_key), + MessageSettings.DEFAULT.showMatrixAvatars + ), largerEmoji = getBoolean( context.getString(R.string.preference_larger_emoji_key), MessageSettings.DEFAULT.largerEmoji diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/info/user/UserInfoFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/info/user/UserInfoFragment.kt index 755168760854bca03329da0d6161886ef2ecce36..b226ad3c9d550e6cd7d8f6e1762089bc0e0d4d7e 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/info/user/UserInfoFragment.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/info/user/UserInfoFragment.kt @@ -37,7 +37,7 @@ import de.kuschku.quasseldroid.R import de.kuschku.quasseldroid.settings.MessageSettings import de.kuschku.quasseldroid.ui.chat.ChatActivity import de.kuschku.quasseldroid.ui.chat.input.AutoCompleteHelper.Companion.IGNORED_CHARS -import de.kuschku.quasseldroid.util.AvatarHelper +import de.kuschku.quasseldroid.util.avatars.AvatarHelper import de.kuschku.quasseldroid.util.helper.* import de.kuschku.quasseldroid.util.irc.format.ContentFormatter import de.kuschku.quasseldroid.util.service.ServiceBoundFragment diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/AutoCompleteHelper.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/AutoCompleteHelper.kt index 9d61428b0d8a1737a496558956ef296d73621811..44a0b934b975b588ee221ceb71fdf47311c3aa3b 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/AutoCompleteHelper.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/input/AutoCompleteHelper.kt @@ -33,7 +33,7 @@ import de.kuschku.libquassel.util.helpers.value import de.kuschku.quasseldroid.R import de.kuschku.quasseldroid.settings.AutoCompleteSettings import de.kuschku.quasseldroid.settings.MessageSettings -import de.kuschku.quasseldroid.util.AvatarHelper +import de.kuschku.quasseldroid.util.avatars.AvatarHelper import de.kuschku.quasseldroid.util.helper.styledAttributes import de.kuschku.quasseldroid.util.helper.toLiveData import de.kuschku.quasseldroid.util.irc.format.IrcFormatDeserializer diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageListFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageListFragment.kt index 89bcbe7cbdcd4e4c000c54835afc6bc8bbe26582..d66fb83c7094b611c2b89631412b296202902a28 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageListFragment.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/MessageListFragment.kt @@ -53,11 +53,12 @@ import de.kuschku.quasseldroid.settings.AutoCompleteSettings import de.kuschku.quasseldroid.settings.BacklogSettings import de.kuschku.quasseldroid.settings.MessageSettings import de.kuschku.quasseldroid.ui.chat.ChatActivity -import de.kuschku.quasseldroid.util.AvatarHelper +import de.kuschku.quasseldroid.util.avatars.AvatarHelper import de.kuschku.quasseldroid.util.helper.* import de.kuschku.quasseldroid.util.service.ServiceBoundFragment import de.kuschku.quasseldroid.util.ui.LinkLongClickMenuHelper import de.kuschku.quasseldroid.util.ui.SpanFormatter +import de.kuschku.quasseldroid.viewmodel.data.Avatar import io.reactivex.BackpressureStrategy import org.threeten.bp.ZoneId import org.threeten.bp.ZonedDateTime @@ -402,14 +403,14 @@ class MessageListFragment : ServiceBoundFragment() { requireContext().resources.displayMetrics ).roundToInt() - val sizeProvider = FixedPreloadSizeProvider<List<String>>(avatarSize, avatarSize) + val sizeProvider = FixedPreloadSizeProvider<List<Avatar>>(avatarSize, avatarSize) - val preloadModelProvider = object : ListPreloader.PreloadModelProvider<List<String>> { + val preloadModelProvider = object : ListPreloader.PreloadModelProvider<List<Avatar>> { override fun getPreloadItems(position: Int) = listOfNotNull( adapter[position]?.content?.let { AvatarHelper.avatar(messageSettings, it, avatarSize) } ) - override fun getPreloadRequestBuilder(item: List<String>) = + override fun getPreloadRequestBuilder(item: List<Avatar>) = GlideApp.with(this@MessageListFragment).loadWithFallbacks(item)?.override(avatarSize) } diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/QuasselMessageRenderer.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/QuasselMessageRenderer.kt index bbe2414d1ef274eab22437ef7df69af9f4c7b855..d27fb4107665d60f7938d301b2571a9d3aeb0ad7 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/QuasselMessageRenderer.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/messages/QuasselMessageRenderer.kt @@ -38,7 +38,7 @@ import de.kuschku.libquassel.util.irc.HostmaskHelper import de.kuschku.quasseldroid.R import de.kuschku.quasseldroid.persistence.QuasselDatabase import de.kuschku.quasseldroid.settings.MessageSettings -import de.kuschku.quasseldroid.util.AvatarHelper +import de.kuschku.quasseldroid.util.avatars.AvatarHelper import de.kuschku.quasseldroid.util.helper.styledAttributes import de.kuschku.quasseldroid.util.helper.visibleIf import de.kuschku.quasseldroid.util.irc.format.ContentFormatter diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/nicks/NickListFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/nicks/NickListFragment.kt index 14a4c95ee7bfd0e03121ee7fdeb2f6e4a187876b..e7ef5a4e0c60094d3ad171232f7ad49f1990b2ea 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/nicks/NickListFragment.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/nicks/NickListFragment.kt @@ -48,13 +48,14 @@ import de.kuschku.quasseldroid.settings.AppearanceSettings import de.kuschku.quasseldroid.settings.MessageSettings import de.kuschku.quasseldroid.ui.chat.info.user.UserInfoActivity import de.kuschku.quasseldroid.ui.chat.input.AutoCompleteHelper.Companion.IGNORED_CHARS -import de.kuschku.quasseldroid.util.AvatarHelper +import de.kuschku.quasseldroid.util.avatars.AvatarHelper import de.kuschku.quasseldroid.util.helper.loadWithFallbacks import de.kuschku.quasseldroid.util.helper.styledAttributes import de.kuschku.quasseldroid.util.helper.toLiveData import de.kuschku.quasseldroid.util.irc.format.IrcFormatDeserializer import de.kuschku.quasseldroid.util.service.ServiceBoundFragment import de.kuschku.quasseldroid.util.ui.TextDrawable +import de.kuschku.quasseldroid.viewmodel.data.Avatar import javax.inject.Inject class NickListFragment : ServiceBoundFragment() { @@ -159,14 +160,14 @@ class NickListFragment : ServiceBoundFragment() { nickList.layoutManager.onRestoreInstanceState(getParcelable(KEY_STATE_LIST)) } - val sizeProvider = FixedPreloadSizeProvider<List<String>>(avatarSize, avatarSize) + val sizeProvider = FixedPreloadSizeProvider<List<Avatar>>(avatarSize, avatarSize) - val preloadModelProvider = object : ListPreloader.PreloadModelProvider<List<String>> { + val preloadModelProvider = object : ListPreloader.PreloadModelProvider<List<Avatar>> { override fun getPreloadItems(position: Int) = listOfNotNull( nickListAdapter[position]?.let { AvatarHelper.avatar(messageSettings, it) } ) - override fun getPreloadRequestBuilder(item: List<String>) = + override fun getPreloadRequestBuilder(item: List<Avatar>) = GlideApp.with(this@NickListFragment).loadWithFallbacks(item)?.override(avatarSize) } diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/clientsettings/about/AboutFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/clientsettings/about/AboutFragment.kt index cf1e4bcc876b94af73537313bd04d7f7f582220b..7ec0c6ff99db5f2b66917e5d6691269d0d7cefc3 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/ui/clientsettings/about/AboutFragment.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/clientsettings/about/AboutFragment.kt @@ -276,6 +276,12 @@ class AboutFragment : DaggerFragment() { ), url = "https://github.com/ReactiveX/RxJava" ), + Library( + name = "Retrofit", + version = "2.4.0", + license = apache2, + url = "https://square.github.io/retrofit/" + ), Library( name = "RxJava", version = "2.1.9", diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/Patterns.kt b/app/src/main/java/de/kuschku/quasseldroid/util/Patterns.kt index 3486e5b637c1154dcd2261bc95953fe65a2b3431..3cab7a984dc57eee6083e0edfb3c64c215741209 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/util/Patterns.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/util/Patterns.kt @@ -67,7 +67,9 @@ object Patterns { @Language("RegExp") const val LOCAL_HOST_NAME = """(?:$IRI_LABEL\.)*$IRI_LABEL""" - val DOMAIN_NAME = Regex("""(?:$LOCAL_HOST_NAME|$HOST_NAME|$IP_ADDRESS_STRING)""") + @Language("RegExp") + const val DOMAIN_NAME_STR = """(?:$LOCAL_HOST_NAME|$HOST_NAME|$IP_ADDRESS_STRING)""" + val DOMAIN_NAME = Regex(DOMAIN_NAME_STR) /** * Regular expression for valid email characters. Does not include some of the valid characters * defined in RFC5321: #&~!^`{}/=$*?| @@ -94,11 +96,21 @@ object Patterns { * Regular expression pattern to match email addresses. It excludes double quoted local parts * and the special characters #&~!^`{}/=$*?| that are included in RFC5321. */ - val AUTOLINK_EMAIL_ADDRESS = Regex("""($WORD_BOUNDARY(?:$EMAIL_ADDRESS_LOCAL_PART@$EMAIL_ADDRESS_DOMAIN)$WORD_BOUNDARY)""") + @Language("RegExp") + val AUTOLINK_EMAIL_ADDRESS_STR = """($WORD_BOUNDARY(?:$EMAIL_ADDRESS_LOCAL_PART@$EMAIL_ADDRESS_DOMAIN)$WORD_BOUNDARY)""" + val AUTOLINK_EMAIL_ADDRESS = Regex(AUTOLINK_EMAIL_ADDRESS_STR) /** * Regular expression pattern to match IRCCloud user idents. */ + @Language("RegExp") const val IRCCLOUD_IDENT_STR = """(?:~?)[us]id(\d+)""" val IRCCLOUD_IDENT = Regex(IRCCLOUD_IDENT_STR) + + /** + * Regular expression pattern to match Matrix user realnames. + */ + @Language("RegExp") + const val MATRIX_REALNAME_STR = """^@[^:]+:$DOMAIN_NAME_STR$""" + val MATRIX_REALNAME = Regex(MATRIX_REALNAME_STR) } diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/AvatarHelper.kt b/app/src/main/java/de/kuschku/quasseldroid/util/avatars/AvatarHelper.kt similarity index 65% rename from app/src/main/java/de/kuschku/quasseldroid/util/AvatarHelper.kt rename to app/src/main/java/de/kuschku/quasseldroid/util/avatars/AvatarHelper.kt index ed4f4e536b55fb903d657feecba299d93f3d7ec1..79edf1367ea1ddf55010bee7a8f7d46b8a990cf4 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/util/AvatarHelper.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/util/avatars/AvatarHelper.kt @@ -17,48 +17,64 @@ * with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.kuschku.quasseldroid.util +package de.kuschku.quasseldroid.util.avatars import de.kuschku.libquassel.quassel.syncables.IrcUser import de.kuschku.libquassel.util.irc.HostmaskHelper import de.kuschku.libquassel.util.irc.IrcCaseMappers import de.kuschku.quasseldroid.persistence.QuasselDatabase import de.kuschku.quasseldroid.settings.MessageSettings +import de.kuschku.quasseldroid.util.Patterns import de.kuschku.quasseldroid.util.backport.codec.Hex import de.kuschku.quasseldroid.util.helper.letIf import de.kuschku.quasseldroid.util.helper.notBlank +import de.kuschku.quasseldroid.viewmodel.data.Avatar import de.kuschku.quasseldroid.viewmodel.data.IrcUserItem import org.apache.commons.codec.digest.DigestUtils object AvatarHelper { fun avatar(settings: MessageSettings, message: QuasselDatabase.NotificationData, size: Int? = null) = listOfNotNull( - message.avatarUrl.notBlank()?.let { listOf(it) }, + message.avatarUrl.notBlank()?.let { listOf(Avatar.NativeAvatar(it)) }, settings.showIRCCloudAvatars.letIf { - ircCloudFallback(HostmaskHelper.user(message.sender), size) + ircCloudFallback(HostmaskHelper.user(message.sender), + size) }, settings.showGravatarAvatars.letIf { gravatarFallback(message.realName, size) + }, + settings.showMatrixAvatars.letIf { + matrixFallback(message.realName, size) } ).flatten() fun avatar(settings: MessageSettings, message: QuasselDatabase.MessageData, size: Int? = null) = listOfNotNull( - message.avatarUrl.notBlank()?.let { listOf(it) }, + message.avatarUrl.notBlank()?.let { listOf(Avatar.NativeAvatar(it)) }, settings.showIRCCloudAvatars.letIf { - ircCloudFallback(HostmaskHelper.user(message.sender), size) + ircCloudFallback(HostmaskHelper.user(message.sender), + size) }, settings.showGravatarAvatars.letIf { gravatarFallback(message.realName, size) + }, + settings.showMatrixAvatars.letIf { + matrixFallback(message.realName, size) } ).flatten() fun avatar(settings: MessageSettings, user: IrcUserItem, size: Int? = null) = listOfNotNull( settings.showIRCCloudAvatars.letIf { - ircCloudFallback(HostmaskHelper.user(user.hostmask), size) + ircCloudFallback(HostmaskHelper.user(user.hostmask), + size) }, settings.showGravatarAvatars.letIf { - gravatarFallback(user.realname.toString(), size) + gravatarFallback(user.realname.toString(), + size) + }, + settings.showMatrixAvatars.letIf { + matrixFallback(user.realname.toString(), + size) } ).flatten() @@ -68,10 +84,32 @@ object AvatarHelper { }, settings.showGravatarAvatars.letIf { gravatarFallback(user.realName(), size) + }, + settings.showMatrixAvatars.letIf { + matrixFallback(user.realName(), size) } ).flatten() - private fun gravatarFallback(realname: String, size: Int?): List<String> { + private fun ircCloudFallback(ident: String, size: Int?): List<Avatar> { + val userId = Patterns.IRCCLOUD_IDENT.matchEntire(ident)?.groupValues?.lastOrNull() + ?: return emptyList() + + if (size != null) { + return listOf( + Avatar.IRCCloudAvatar( + "https://static.irccloud-cdn.com/avatar-redirect/s${truncateSize(size)}/$userId" + ) + ) + } + + return listOf( + Avatar.IRCCloudAvatar( + "https://static.irccloud-cdn.com/avatar-redirect/$userId" + ) + ) + } + + private fun gravatarFallback(realname: String, size: Int?): List<Avatar> { return Patterns.AUTOLINK_EMAIL_ADDRESS .findAll(realname) .mapNotNull { @@ -83,18 +121,17 @@ object AvatarHelper { } else { "https://www.gravatar.com/avatar/$hash?d=404&s=${truncateSize(size)}" } - }.toList() + }.map { Avatar.GravatarAvatar(it) }.toList() } - private fun ircCloudFallback(ident: String, size: Int?): List<String> { - val userId = Patterns.IRCCLOUD_IDENT.matchEntire(ident)?.groupValues?.lastOrNull() - ?: return emptyList() - - if (size != null) { - return listOf("https://static.irccloud-cdn.com/avatar-redirect/s${truncateSize(size)}/$userId") + private fun matrixFallback(realname: String, size: Int?): List<Avatar> { + return if (Patterns.MATRIX_REALNAME.matches(realname)) { + listOf( + Avatar.MatrixAvatar(realname, size?.let(this::truncateSize)) + ) + } else { + emptyList() } - - return listOf("https://static.irccloud-cdn.com/avatar-redirect/$userId") } private fun truncateSize(originalSize: Int) = if (originalSize > 72) 512 else 72 diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/avatars/MatrixApi.kt b/app/src/main/java/de/kuschku/quasseldroid/util/avatars/MatrixApi.kt new file mode 100644 index 0000000000000000000000000000000000000000..842a5e0bfb6a408582c070459743bb83e7de8746 --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid/util/avatars/MatrixApi.kt @@ -0,0 +1,46 @@ +/* + * Quasseldroid - Quassel client for Android + * + * Copyright (c) 2018 Janne Koschinski + * Copyright (c) 2018 The Quassel Project + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3 as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package de.kuschku.quasseldroid.util.avatars + +import okhttp3.ResponseBody +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Path +import retrofit2.http.Query + +interface MatrixApi { + @GET("/_matrix/client/r0/profile/{name}/avatar_url") + fun avatarUrl(@Path("name") name: String): Call<MatrixAvatarResponse> + + @GET("/_matrix/media/r0/thumbnail/{server}/{id}/?width=32&height=32&method=crop") + fun avatarThumbnail( + @Path("server") server: String, + @Path("id") id: String, + @Query("width") width: Int = 512, + @Query("height") height: Int = 512, + @Query("method") method: String = "scale" + ): Call<ResponseBody> + + @GET("/_matrix/media/r0/download/{server}/{id}") + fun avatarImage( + @Path("server") server: String, + @Path("id") id: String + ): Call<ResponseBody> +} diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/avatars/MatrixAvatarInfo.kt b/app/src/main/java/de/kuschku/quasseldroid/util/avatars/MatrixAvatarInfo.kt new file mode 100644 index 0000000000000000000000000000000000000000..b87619ed4614a3bfa2885ff7edbf076b253d12b8 --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid/util/avatars/MatrixAvatarInfo.kt @@ -0,0 +1,25 @@ +/* + * Quasseldroid - Quassel client for Android + * + * Copyright (c) 2018 Janne Koschinski + * Copyright (c) 2018 The Quassel Project + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3 as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package de.kuschku.quasseldroid.util.avatars + +data class MatrixAvatarInfo( + val avatarUrl: String, + val size: Int? +) diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/avatars/MatrixAvatarResponse.kt b/app/src/main/java/de/kuschku/quasseldroid/util/avatars/MatrixAvatarResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..dacaf521c9e21fa910c46e2ee8e5a9a600d3a0b2 --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid/util/avatars/MatrixAvatarResponse.kt @@ -0,0 +1,28 @@ +/* + * Quasseldroid - Quassel client for Android + * + * Copyright (c) 2018 Janne Koschinski + * Copyright (c) 2018 The Quassel Project + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3 as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package de.kuschku.quasseldroid.util.avatars + +import com.google.gson.annotations.SerializedName + +data class MatrixAvatarResponse( + @SerializedName("avatar_url") + val avatarUrl: String? +) + diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/avatars/MatrixDataFetcher.kt b/app/src/main/java/de/kuschku/quasseldroid/util/avatars/MatrixDataFetcher.kt new file mode 100644 index 0000000000000000000000000000000000000000..0835d624cfddc57463a8670a387f6b0f8276bb67 --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid/util/avatars/MatrixDataFetcher.kt @@ -0,0 +1,79 @@ +/* + * Quasseldroid - Quassel client for Android + * + * Copyright (c) 2018 Janne Koschinski + * Copyright (c) 2018 The Quassel Project + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3 as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package de.kuschku.quasseldroid.util.avatars + +import android.net.Uri +import com.bumptech.glide.Priority +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.data.DataFetcher +import de.kuschku.quasseldroid.viewmodel.data.Avatar +import okhttp3.Call +import java.io.InputStream + +class MatrixDataFetcher( + private val model: Avatar.MatrixAvatar, + private val api: MatrixApi +) : + DataFetcher<InputStream> { + private var call: Call? = null + + override fun getDataClass(): Class<InputStream> { + return InputStream::class.java + } + + override fun cleanup() { + } + + override fun getDataSource(): DataSource { + return DataSource.REMOTE + } + + override fun cancel() { + call?.cancel() + } + + private fun loadAvatarInfo(model: Avatar.MatrixAvatar) = + api.avatarUrl(model.userId).execute().body()?.let { + it.avatarUrl?.let { + MatrixAvatarInfo(it, model.size) + } + } + + private fun inputStreamFromAvatarInfo(info: MatrixAvatarInfo): InputStream? { + val url = Uri.parse(info.avatarUrl) + return if (info.size != null && info.size < 512) { + api.avatarThumbnail(server = url.host, + id = url.pathSegments.first(), + width = info.size, + height = info.size, + method = if (info.size > 96) "scale" else "crop") + } else { + api.avatarImage(server = url.host, id = url.pathSegments.first()) + }.execute().body()?.byteStream() + } + + override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) { + loadAvatarInfo(model)?.let { + inputStreamFromAvatarInfo(it) + }?.let { + callback.onDataReady(it) + } ?: callback.onLoadFailed(IllegalStateException("Unknown Error!")) + } +} diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/avatars/MatrixModelLoader.kt b/app/src/main/java/de/kuschku/quasseldroid/util/avatars/MatrixModelLoader.kt new file mode 100644 index 0000000000000000000000000000000000000000..8e05c6436c888fc99514040d2af6a664d9e2686b --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid/util/avatars/MatrixModelLoader.kt @@ -0,0 +1,37 @@ +/* + * Quasseldroid - Quassel client for Android + * + * Copyright (c) 2018 Janne Koschinski + * Copyright (c) 2018 The Quassel Project + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3 as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package de.kuschku.quasseldroid.util.avatars + +import com.bumptech.glide.load.Options +import com.bumptech.glide.load.model.ModelLoader +import com.bumptech.glide.signature.ObjectKey +import de.kuschku.quasseldroid.viewmodel.data.Avatar +import java.io.InputStream + +class MatrixModelLoader(val api: MatrixApi) : ModelLoader<Avatar.MatrixAvatar, InputStream> { + override fun buildLoadData(model: Avatar.MatrixAvatar, width: Int, height: Int, + options: Options): ModelLoader.LoadData<InputStream>? { + return ModelLoader.LoadData(ObjectKey(model), MatrixDataFetcher(model, api)) + } + + override fun handles(model: Avatar.MatrixAvatar): Boolean { + return true + } +} diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/helper/GlideHelper.kt b/app/src/main/java/de/kuschku/quasseldroid/util/helper/GlideHelper.kt index 78278bcbbfd61fcb31fbd0ecccccc55506288f9c..11f013106b3f6aa7f31227f660218786f549c283 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/util/helper/GlideHelper.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/util/helper/GlideHelper.kt @@ -25,10 +25,16 @@ import com.bumptech.glide.RequestBuilder import de.kuschku.quasseldroid.GlideApp import de.kuschku.quasseldroid.GlideRequest import de.kuschku.quasseldroid.GlideRequests +import de.kuschku.quasseldroid.viewmodel.data.Avatar -fun GlideRequests.loadWithFallbacks(urls: List<String>): GlideRequest<Drawable>? { - fun fold(url: String, fallback: RequestBuilder<Drawable>?): GlideRequest<Drawable> { - return this.load(url).let { +fun GlideRequests.loadWithFallbacks(urls: List<Avatar>): GlideRequest<Drawable>? { + fun fold(url: Avatar, fallback: RequestBuilder<Drawable>?): GlideRequest<Drawable> { + return when (url) { + is Avatar.NativeAvatar -> this.load(url.url) + is Avatar.GravatarAvatar -> this.load(url.url) + is Avatar.IRCCloudAvatar -> this.load(url.url) + is Avatar.MatrixAvatar -> this.load(url) + }.let { if (fallback != null) it.error(fallback) else it } } @@ -36,7 +42,7 @@ fun GlideRequests.loadWithFallbacks(urls: List<String>): GlideRequest<Drawable>? return urls.foldRight(null, ::fold) } -fun ImageView.loadAvatars(urls: List<String>, fallback: Drawable? = null, crop: Boolean = true) { +fun ImageView.loadAvatars(urls: List<Avatar>, fallback: Drawable? = null, crop: Boolean = true) { if (urls.isNotEmpty()) { GlideApp.with(this) .loadWithFallbacks(urls) diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/service/BackendServiceConnection.kt b/app/src/main/java/de/kuschku/quasseldroid/util/service/BackendServiceConnection.kt index bcb0b75bfa0f47070863f07c93e4b95c747949ea..7fdbf5fc44680cd91218f3037aede2eb50cd5387 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/util/service/BackendServiceConnection.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/util/service/BackendServiceConnection.kt @@ -35,12 +35,15 @@ class BackendServiceConnection : ServiceConnection { var context: Context? = null - var bound: Boolean = false + private var bound: Boolean = false + + private var binding: Boolean = false override fun onServiceDisconnected(component: ComponentName?) { when (component) { ComponentName(context, QuasselService::class.java) -> { bound = false + binding = false backend.onNext(Optional.empty()) } } @@ -51,6 +54,7 @@ class BackendServiceConnection : ServiceConnection { ComponentName(context, QuasselService::class.java) -> if (binder is QuasselBinder) { bound = true + binding = false backend.onNext(Optional.of(binder.backend)) } } @@ -60,15 +64,23 @@ class BackendServiceConnection : ServiceConnection { context?.startService(intent) } + @Synchronized fun bind(intent: Intent = QuasselService.intent(context!!), flags: Int = 0) { - if (!this.bound) context?.bindService(intent, this, flags) + if (!this.bound && !this.binding) { + this.binding = true + context?.bindService(intent, this, flags) + } } fun stop(intent: Intent = QuasselService.intent(context!!)) { context?.stopService(intent) } + @Synchronized fun unbind() { - if (this.bound) context?.unbindService(this) + if (this.bound && !this.binding) { + this.binding = true + context?.unbindService(this) + } } } diff --git a/app/src/main/res/values-de/strings_preferences.xml b/app/src/main/res/values-de/strings_preferences.xml index 3295059cb8fc54c6f16f084c120f551cdb7b99e3..184f098572c3a127415fff48eeaca50e8e8cdc6c 100644 --- a/app/src/main/res/values-de/strings_preferences.xml +++ b/app/src/main/res/values-de/strings_preferences.xml @@ -93,6 +93,9 @@ <string name="preference_show_gravatar_avatars_title">Gravatar Avatare anzeigen</string> <string name="preference_show_gravatar_avatars_summary">Zeigt für Nutzer ohne Avatare den Gravatar-Avatar an, falls verfügbar</string> + <string name="preference_show_matrix_avatars_title">Matrix Avatare anzeigen</string> + <string name="preference_show_matrix_avatars_summary">Zeigt für Nutzer ohne Avatare den Matrix-Avatar an, falls verfügbar</string> + <string name="preference_time_at_end_title">Rechts-Ausgerichtete Zeit</string> <string name="preference_time_at_end_summary">Zeigt die Zeit rechts in Nachrichten an</string> diff --git a/app/src/main/res/values/strings_preferences.xml b/app/src/main/res/values/strings_preferences.xml index 9ba628c8706d1ebc07ed247f91badcbef8434a5a..c638b1a7d5886784cd14346402573c10830ddd35 100644 --- a/app/src/main/res/values/strings_preferences.xml +++ b/app/src/main/res/values/strings_preferences.xml @@ -177,6 +177,10 @@ <string name="preference_show_gravatar_avatars_title">Show Gravatar Avatars</string> <string name="preference_show_gravatar_avatars_summary">Shows for users without avatar their Gravatar fallback, if available</string> + <string name="preference_show_matrix_avatars_key" translatable="false">show_matrix_avatars</string> + <string name="preference_show_matrix_avatars_title">Show Matrix Avatars</string> + <string name="preference_show_matrix_avatars_summary">Shows for users without avatar their Matrix fallback, if available</string> + <string name="preference_time_at_end_key" translatable="false">time_at_end</string> <string name="preference_time_at_end_title">Right-Aligned Timestamps</string> <string name="preference_time_at_end_summary">Aligns timestamps at the end of each message</string> diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index a270c1f55307028ed70fb198cc28c2b4e08c9254..ba8aa707798d29a8e37858aaf5a03e53f8d0881d 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -128,6 +128,12 @@ android:key="@string/preference_show_gravatar_avatars_key" android:title="@string/preference_show_gravatar_avatars_title" /> + <SwitchPreference + android:defaultValue="false" + android:dependency="@string/preference_show_avatars_key" + android:key="@string/preference_show_matrix_avatars_key" + android:title="@string/preference_show_matrix_avatars_title" /> + <SwitchPreference android:defaultValue="true" android:key="@string/preference_nicks_on_new_line_key" diff --git a/app/src/test/java/de/kuschku/quasseldroid/util/AvatarHelperTest.kt b/app/src/test/java/de/kuschku/quasseldroid/util/AvatarHelperTest.kt index 6fb9fcc1b14131012538dc1411146840122d424e..cdcf7b3f9936db812e3a7e671c077532bdf57563 100644 --- a/app/src/test/java/de/kuschku/quasseldroid/util/AvatarHelperTest.kt +++ b/app/src/test/java/de/kuschku/quasseldroid/util/AvatarHelperTest.kt @@ -23,6 +23,7 @@ import de.kuschku.libquassel.protocol.Message_Flag import de.kuschku.libquassel.protocol.Message_Type import de.kuschku.quasseldroid.persistence.QuasselDatabase import de.kuschku.quasseldroid.settings.MessageSettings +import de.kuschku.quasseldroid.util.avatars.AvatarHelper import org.junit.Test import org.threeten.bp.Instant diff --git a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/data/AutoCompleteItem.kt b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/data/AutoCompleteItem.kt index 6dc1a75b25bd038f4353512535fa65ae637d9a4b..afa1e13f0d35ee1e4fe879ba858fc4894bcb0c54 100644 --- a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/data/AutoCompleteItem.kt +++ b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/data/AutoCompleteItem.kt @@ -41,7 +41,7 @@ sealed class AutoCompleteItem(open val name: String) : Comparable<AutoCompleteIt val realname: CharSequence, val away: Boolean, val networkCasemapping: String?, - val avatarUrls: List<String> = emptyList(), + val avatarUrls: List<Avatar> = emptyList(), val fallbackDrawable: Drawable? = null, val displayNick: CharSequence? = null ) : AutoCompleteItem(nick) diff --git a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/data/Avatar.kt b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/data/Avatar.kt new file mode 100644 index 0000000000000000000000000000000000000000..8c83036467212117567fd718f418f5825186462d --- /dev/null +++ b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/data/Avatar.kt @@ -0,0 +1,27 @@ +/* + * Quasseldroid - Quassel client for Android + * + * Copyright (c) 2018 Janne Koschinski + * Copyright (c) 2018 The Quassel Project + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3 as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package de.kuschku.quasseldroid.viewmodel.data + +sealed class Avatar { + data class NativeAvatar(val url: String) : Avatar() + data class IRCCloudAvatar(val url: String) : Avatar() + data class GravatarAvatar(val url: String) : Avatar() + data class MatrixAvatar(val userId: String, val size: Int?) : Avatar() +} diff --git a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/data/FormattedMessage.kt b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/data/FormattedMessage.kt index 6eda804f1afd8106f707405d901d541f34360ea0..2a9088b19f339ec62ab24751dfdf417116c5dcb5 100644 --- a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/data/FormattedMessage.kt +++ b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/data/FormattedMessage.kt @@ -30,7 +30,7 @@ class FormattedMessage( val combined: CharSequence, val fallbackDrawable: Drawable? = null, val realName: CharSequence? = null, - val avatarUrls: List<String> = emptyList(), + val avatarUrls: List<Avatar> = emptyList(), val isSelected: Boolean, val isExpanded: Boolean, val isMarkerLine: Boolean diff --git a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/data/IrcUserItem.kt b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/data/IrcUserItem.kt index f7c4474118aadfefe14cbb3cd97d987dd720bc22..121ca2bed059d5e4924872188ae6407beb28ee75 100644 --- a/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/data/IrcUserItem.kt +++ b/viewmodel/src/main/java/de/kuschku/quasseldroid/viewmodel/data/IrcUserItem.kt @@ -29,7 +29,7 @@ data class IrcUserItem( val hostmask: String, val away: Boolean, val networkCasemapping: String?, - val avatarUrls: List<String> = emptyList(), + val avatarUrls: List<Avatar> = emptyList(), val fallbackDrawable: Drawable? = null, val displayNick: CharSequence? = null )