diff --git a/android-smsmms/src/main/java/com/android/mms/service_alt/MmsHttpClient.java b/android-smsmms/src/main/java/com/android/mms/service_alt/MmsHttpClient.java index aa31f9d8b..281392542 100755 --- a/android-smsmms/src/main/java/com/android/mms/service_alt/MmsHttpClient.java +++ b/android-smsmms/src/main/java/com/android/mms/service_alt/MmsHttpClient.java @@ -18,21 +18,24 @@ import android.content.Context; import android.text.TextUtils; + import com.android.mms.service_alt.exception.MmsHttpException; import com.squareup.okhttp.ConnectionPool; import com.squareup.okhttp.ConnectionSpec; +import com.squareup.okhttp.Dns; import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.Protocol; import com.squareup.okhttp.Request; import com.squareup.okhttp.Response; -import com.squareup.okhttp.internal.Internal; import com.squareup.okhttp.internal.huc.HttpURLConnectionImpl; import com.squareup.okhttp.internal.huc.HttpsURLConnectionImpl; + import timber.log.Timber; import javax.net.SocketFactory; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; + import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; @@ -40,6 +43,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; +import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.MalformedURLException; import java.net.ProtocolException; @@ -48,6 +52,7 @@ import java.net.SocketAddress; import java.net.URI; import java.net.URL; +import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -86,13 +91,13 @@ public class MmsHttpClient { /** * Constructor * - * @param context The Context object - * @param socketFactory The socket factory for creating an OKHttp client - * @param hostResolver The host name resolver for creating an OKHttp client + * @param context The Context object + * @param socketFactory The socket factory for creating an OKHttp client + * @param hostResolver The host name resolver for creating an OKHttp client * @param connectionPool The connection pool for creating an OKHttp client */ public MmsHttpClient(Context context, SocketFactory socketFactory, MmsNetworkManager hostResolver, - ConnectionPool connectionPool) { + ConnectionPool connectionPool) { mContext = context; mSocketFactory = socketFactory; mHostResolver = hostResolver; @@ -102,19 +107,19 @@ public MmsHttpClient(Context context, SocketFactory socketFactory, MmsNetworkMan /** * Execute an MMS HTTP request, either a POST (sending) or a GET (downloading) * - * @param urlString The request URL, for sending it is usually the MMSC, and for downloading - * it is the message URL - * @param pdu For POST (sending) only, the PDU to send - * @param method HTTP method, POST for sending and GET for downloading + * @param urlString The request URL, for sending it is usually the MMSC, and for downloading + * it is the message URL + * @param pdu For POST (sending) only, the PDU to send + * @param method HTTP method, POST for sending and GET for downloading * @param isProxySet Is there a proxy for the MMSC - * @param proxyHost The proxy host - * @param proxyPort The proxy port - * @param mmsConfig The MMS config to use + * @param proxyHost The proxy host + * @param proxyPort The proxy port + * @param mmsConfig The MMS config to use * @return The HTTP response body * @throws MmsHttpException For any failures */ public byte[] execute(String urlString, byte[] pdu, String method, boolean isProxySet, - String proxyHost, int proxyPort, MmsConfig.Overridden mmsConfig) + String proxyHost, int proxyPort, MmsConfig.Overridden mmsConfig) throws MmsHttpException { Timber.d("HTTP: " + method + " " + urlString + (isProxySet ? (", proxy=" + proxyHost + ":" + proxyPort) : "") @@ -214,13 +219,13 @@ public byte[] execute(String urlString, byte[] pdu, String method, boolean isPro /** * Open an HTTP connection - * + *

* TODO: The following code is borrowed from android.net.Network.openConnection * Once that method supports proxy, we should use that instead * Also we should remove the associated HostResolver and ConnectionPool from * MmsNetworkManager * - * @param url The URL to connect to + * @param url The URL to connect to * @param proxy The proxy to use * @return The opened HttpURLConnection * @throws MalformedURLException If URL is malformed @@ -228,6 +233,26 @@ public byte[] execute(String urlString, byte[] pdu, String method, boolean isPro private HttpURLConnection openConnection(URL url, final Proxy proxy) throws MalformedURLException { final String protocol = url.getProtocol(); OkHttpClient okHttpClient; + + Dns resolverDns = new Dns() { + @Override + public List lookup(String hostname) throws UnknownHostException { + try { + InetAddress[] addrs = mHostResolver.resolveInetAddresses(hostname); + if (addrs == null || addrs.length == 0) { + throw new UnknownHostException("No addresses for " + hostname); + } + return Arrays.asList(addrs); + } catch (UnknownHostException e) { + throw e; + } catch (Exception e) { + UnknownHostException uhe = new UnknownHostException("Failed to resolve " + hostname); + uhe.initCause(e); + throw uhe; + } + } + }; + if (protocol.equals("http")) { okHttpClient = new OkHttpClient(); okHttpClient.setFollowRedirects(false); @@ -260,8 +285,14 @@ public Request authenticateProxy(Proxy proxy, Response response) throws IOExcept }); okHttpClient.setConnectionSpecs(Arrays.asList(ConnectionSpec.CLEARTEXT)); okHttpClient.setConnectionPool(new ConnectionPool(3, 60000)); - okHttpClient.setSocketFactory(SocketFactory.getDefault()); - Internal.instance.setNetwork(okHttpClient, mHostResolver); + + okHttpClient.setDns(resolverDns); + + if (mSocketFactory != null) { + okHttpClient.setSocketFactory(mSocketFactory); + } else { + okHttpClient.setSocketFactory(SocketFactory.getDefault()); + } if (proxy != null) { okHttpClient.setProxy(proxy); @@ -298,7 +329,8 @@ public Request authenticateProxy(Proxy proxy, Response response) throws IOExcept }); okHttpClient.setConnectionSpecs(Arrays.asList(ConnectionSpec.CLEARTEXT)); okHttpClient.setConnectionPool(new ConnectionPool(3, 60000)); - Internal.instance.setNetwork(okHttpClient, mHostResolver); + + okHttpClient.setDns(resolverDns); return new HttpsURLConnectionImpl(url, okHttpClient); } else { @@ -385,6 +417,7 @@ private static void addLocaleToHttpAcceptLanguage(StringBuilder builder, Locale } private static final Pattern MACRO_P = Pattern.compile("##(\\S+)##"); + /** * Resolve the macro in HTTP param value text * For example, "something##LINE1##something" is resolved to "something9139531419something" @@ -393,7 +426,7 @@ private static void addLocaleToHttpAcceptLanguage(StringBuilder builder, Locale * @return The HTTP param with macro resolved to real value */ private static String resolveMacro(Context context, String value, - MmsConfig.Overridden mmsConfig) { + MmsConfig.Overridden mmsConfig) { if (TextUtils.isEmpty(value)) { return value; } @@ -429,7 +462,7 @@ private static String resolveMacro(Context context, String value, * macros like "##LINE1##" or "##NAI##" which is resolved with methods in this class * * @param connection The HttpURLConnection that we add headers to - * @param mmsConfig The MmsConfig object + * @param mmsConfig The MmsConfig object */ private void addExtraHeaders(HttpURLConnection connection, MmsConfig.Overridden mmsConfig) { final String extraHttpParams = mmsConfig.getHttpParams(); diff --git a/android-smsmms/src/main/java/com/android/mms/service_alt/MmsNetworkManager.java b/android-smsmms/src/main/java/com/android/mms/service_alt/MmsNetworkManager.java index e3092499d..2c541ea71 100755 --- a/android-smsmms/src/main/java/com/android/mms/service_alt/MmsNetworkManager.java +++ b/android-smsmms/src/main/java/com/android/mms/service_alt/MmsNetworkManager.java @@ -23,7 +23,6 @@ import android.net.NetworkInfo; import android.net.NetworkRequest; import android.net.SSLCertificateSocketFactory; -import android.os.Build; import android.os.SystemClock; import com.android.mms.service_alt.exception.MmsNetworkException; import com.squareup.okhttp.ConnectionPool; @@ -32,7 +31,7 @@ import java.net.InetAddress; import java.net.UnknownHostException; -public class MmsNetworkManager implements com.squareup.okhttp.internal.Network { +public class MmsNetworkManager { // Timeout used to call ConnectivityManager.requestNetwork private static final int NETWORK_REQUEST_TIMEOUT_MILLIS = 60 * 1000; @@ -236,7 +235,6 @@ private void resetLocked() { private static final InetAddress[] EMPTY_ADDRESS_ARRAY = new InetAddress[0]; - @Override public InetAddress[] resolveInetAddresses(String host) throws UnknownHostException { Network network = null; synchronized (this) { diff --git a/build.gradle.kts b/build.gradle.kts index da2223bd6..831d41720 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,9 +1,10 @@ -import org.jetbrains.kotlin.gradle.plugin.KaptExtension +import com.android.build.gradle.BaseExtension +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile // Needed until we upstream buildscript { dependencies { - classpath("io.realm:realm-gradle-plugin:10.15.0") + classpath("io.realm:realm-gradle-plugin:10.19.0") classpath("com.google.firebase:firebase-crashlytics-gradle:2.5.2") } } @@ -11,21 +12,33 @@ buildscript { plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.android.library) apply false - alias(libs.plugins.kotlin.android) version "1.7.21" apply false - alias(libs.plugins.google.services) version "4.3.14" apply false + alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.google.services) apply false } tasks.register("clean") { - delete(rootProject.buildDir) + delete(rootProject.layout.buildDirectory) } subprojects { - afterEvaluate { - extensions.findByType(KaptExtension::class.java)?.apply { - javacOptions { - option("-source", "8") - option("-target", "8") + tasks.withType().configureEach { + kotlinOptions.jvmTarget = "17" + } + + plugins.withId("com.android.application") { + extensions.configure(BaseExtension::class.java) { + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } } } -} + plugins.withId("com.android.library") { + extensions.configure(BaseExtension::class.java) { + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + } + } +} \ No newline at end of file diff --git a/data/build.gradle.kts b/data/build.gradle.kts index 84d169d50..edd113b38 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -30,11 +30,6 @@ android { version = release(36) } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - defaultConfig { minSdk = 23 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts index 43e89cc8c..6f5b22d93 100644 --- a/domain/build.gradle.kts +++ b/domain/build.gradle.kts @@ -31,11 +31,6 @@ android { version = release(36) } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - defaultConfig { minSdk = 23 } diff --git a/domain/src/main/java/com/moez/QKSMS/util/FlowPreference.kt b/domain/src/main/java/com/moez/QKSMS/util/FlowPreference.kt new file mode 100644 index 000000000..a579240cb --- /dev/null +++ b/domain/src/main/java/com/moez/QKSMS/util/FlowPreference.kt @@ -0,0 +1,14 @@ +package com.moez.QKSMS.util + +import com.f2prateek.rx.preferences2.Preference +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.rx2.asFlow + +class FlowPreference( + private val pref: Preference +) { + val flow: Flow = pref.asObservable().asFlow() + + fun get(): T = pref.get() + fun set(value: T) = pref.set(value) +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6734a85bf..ca4c18c17 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,46 +1,45 @@ [versions] agp = "8.13.1" -kotlin = "1.7.21" +kotlin = "1.9.25" appcompat = "1.7.1" -autodispose-android-archcomponents = "1.3.0" -conductor = "2.1.5" -constraintlayout = "1.1.3" +autodispose-android-archcomponents = "1.4.1" +conductor = "3.2.0" +constraintlayout = "2.2.1" dagger = "2.57.2" datashare = "1.3.0" -documentfile = "1.0.1" -emoji2-bundled = "1.4.0" -exifinterface = "1.0.0" +documentfile = "1.1.0" +emoji2-bundled = "1.6.0" +exifinterface = "1.4.2" exoplayer-core = "r2.9.0" flexbox = "3.0.0" -glide = "4.16.0" +glide = "5.0.5" javax-annotation-api = "1.3.2" -junit = "4.12" -kotlin-stdlib = "1.7.21" -kotlinx-coroutines-core = "1.4.3" -ktx = "1.10.1" -libphonenumber-android = "8.13.47" -lifecycle-extensions = "2.1.0" +junit = "4.13.2" +kotlinx-coroutines-core = "1.8.0" +ktx = "1.17.0" +libphonenumber-android = "9.0.17" +lifecycle = "2.1.0" material = "1.13.0" -mockito-android = "2.18.3" -moshi = "1.8.0" -okhttp = "2.5.0" -okhttp-version = "4.10.0" +mockito-android = "5.7.0" +moshi = "1.15.2" +okhttp2 = "2.7.5" +okhttp3 = "4.12.0" photoview = "2.1.4" realm-android-adapters = "3.1.0" realm-annotations = "10.19.0" -robolectric = "4.10.3" -runner = "1.1.0-alpha3" -rx-preferences = "2.0.0-RC3" -rxandroid = "2.0.1" -rxbinding-kotlin = "2.0.0" -rxdogtag = "0.2.0" -rxjava = "2.1.4" -rxkotlin = "2.1.0" +robolectric = "4.16" +runner = "1.7.0" +rx-preferences = "2.0.1" +rxandroid = "2.1.1" +rxbinding-kotlin = "2.2.0" +rxdogtag = "1.0.2" +rxjava = "2.2.21" +rxkotlin = "2.4.0" shortcutbadger = "1.1.22" timber = "5.0.1" -google-services = "4.3.14" -viewpager2 = "1.0.0-beta05" -work-runtime-ktx = "2.8.0" +google-services = "4.4.4" +viewpager2 = "1.1.0" +work-runtime-ktx = "2.10.5" [libraries] androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } @@ -49,8 +48,11 @@ androidx-documentfile = { module = "androidx.documentfile:documentfile", version androidx-emoji2-bundled = { module = "androidx.emoji2:emoji2-bundled", version.ref = "emoji2-bundled" } androidx-exifinterface = { module = "androidx.exifinterface:exifinterface", version.ref = "exifinterface" } androidx-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "ktx" } -androidx-lifecycle-common-java8 = { module = "androidx.lifecycle:lifecycle-common-java8", version.ref = "lifecycle-extensions" } -androidx-lifecycle-extensions = { module = "androidx.lifecycle:lifecycle-extensions", version.ref = "lifecycle-extensions" } +lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycle" } +lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" } +lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycle" } +lifecycle-common-java8 = { group = "androidx.lifecycle", name = "lifecycle-common-java8", version.ref = "lifecycle" } +lifecycle-extensions = { group = "androidx.lifecycle", name = "lifecycle-extensions", version.ref = "lifecycle" } androidx-viewpager2 = { module = "androidx.viewpager2:viewpager2", version.ref = "viewpager2" } androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "work-runtime-ktx" } androidx-work-rxjava2 = { module = "androidx.work:work-rxjava2", version.ref = "work-runtime-ktx" } @@ -71,8 +73,8 @@ exoplayer-core = { module = "com.github.google.ExoPlayer:exoplayer-core", versio flexbox = { module = "com.google.android.flexbox:flexbox", version.ref = "flexbox" } glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" } javax-annotation-api = { module = "javax.annotation:javax.annotation-api", version.ref = "javax-annotation-api" } -kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin-stdlib" } -kotlin-stdlib-jdk7 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk7", version.ref = "kotlin-stdlib" } +kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } +kotlin-stdlib-jdk7 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk7", version.ref = "kotlin" } kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines-core" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines-core" } kotlinx-coroutines-reactive = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-reactive", version.ref = "kotlinx-coroutines-core" } @@ -84,9 +86,9 @@ mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito-and moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" } moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshi" } moshi-kotlin-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshi" } -okhttp = { module = "com.squareup.okhttp:okhttp", version.ref = "okhttp" } -okhttp-urlconnection = { module = "com.squareup.okhttp:okhttp-urlconnection", version.ref = "okhttp" } -okhttp3-okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp-version" } +okhttp = { module = "com.squareup.okhttp:okhttp", version.ref = "okhttp2" } +okhttp-urlconnection = { module = "com.squareup.okhttp:okhttp-urlconnection", version.ref = "okhttp2" } +okhttp3-okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp3" } photoview = { module = "com.github.chrisbanes:photoview", version.ref = "photoview" } realm-android-adapters = { module = "com.github.realm:realm-android-adapters", version.ref = "realm-android-adapters" } realm-android-library = { module = "io.realm:realm-android-library", version.ref = "realm-annotations" } diff --git a/presentation/build.gradle.kts b/presentation/build.gradle.kts index afbe80a1a..a21985684 100644 --- a/presentation/build.gradle.kts +++ b/presentation/build.gradle.kts @@ -83,16 +83,6 @@ android { includeInBundle = false } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - - // kotlinOptions inside android block (Kotlin DSL) - kotlinOptions { - jvmTarget = "1.8" - } - lint { abortOnError = false } @@ -100,8 +90,11 @@ android { dependencies { // lifecycle - implementation(libs.androidx.lifecycle.extensions) - implementation(libs.androidx.lifecycle.common.java8) + implementation(libs.lifecycle.viewmodel.ktx) + implementation(libs.lifecycle.runtime.ktx) + implementation(libs.lifecycle.livedata.ktx) + implementation(libs.lifecycle.common.java8) + implementation(libs.lifecycle.extensions) // androidx implementation(libs.androidx.appcompat) diff --git a/presentation/proguard-rules.pro b/presentation/proguard-rules.pro index fed41f3a1..dd3caedf3 100644 --- a/presentation/proguard-rules.pro +++ b/presentation/proguard-rules.pro @@ -1,7 +1,4 @@ --dontobfuscate - # android-smsmms -# -keep class android.net.** { *; } -dontwarn android.net.ConnectivityManager -dontwarn android.net.LinkProperties @@ -45,8 +42,10 @@ -dontwarn org.slf4j.Logger -dontwarn org.slf4j.LoggerFactory +# Keep @FromJson/@ToJson adapters -keepclasseswithmembers class * { - @com.squareup.moshi.* ; + @com.squareup.moshi.FromJson ; + @com.squareup.moshi.ToJson ; } -keep @com.squareup.moshi.JsonQualifier interface * @@ -106,24 +105,27 @@ (...); ; } -# Dagger -# This is to allow the restore functionality to work + +# Dagger 2 -keep class dagger.** { *; } --keep class * extends dagger.Module { *; } --keep class * extends dagger.Component { *; } --keep class * extends dagger.Subcomponent { *; } --keep class * { + +-keep @dagger.Module class * { *; } +-keep @dagger.Subcomponent interface * { *; } +-keep @dagger.Component interface * { *; } + +-keepclassmembers class * { @dagger.Provides ; } + +# RxJava -keep class io.reactivex.** { *; } -keep class io.reactivex.subjects.** { *; } -keep class androidx.activity.result.** { *; } -keep class org.prauga.messages.** { *; } --keep class io.realm.annotations.RealmModule +# Realm -keep @io.realm.annotations.RealmModule class * -keep @interface io.realm.annotations.RealmModule { *; } --keep class io.realm.annotations.RealmModule { *; } -keep class io.realm.internal.Keep -keep @io.realm.internal.Keep class * { *; } @@ -131,7 +133,6 @@ -keep class io.realm.internal.KeepMember -keep @io.realm.internal.KeepMember class * { @io.realm.internal.KeepMember *; } --dontwarn javax.** -dontwarn io.realm.** -dontwarn io.reactivex.android.** diff --git a/presentation/src/main/java/com/moez/QKSMS/common/MenuItemAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/common/MenuItemAdapter.kt index d824daafc..e1b600bf8 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/MenuItemAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/MenuItemAdapter.kt @@ -24,20 +24,23 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.annotation.ArrayRes import androidx.recyclerview.widget.RecyclerView +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.subjects.PublishSubject +import io.reactivex.subjects.Subject import org.prauga.messages.common.base.QkAdapter import org.prauga.messages.common.base.QkBindingViewHolder import org.prauga.messages.common.util.Colors import org.prauga.messages.common.util.extensions.resolveThemeColor import org.prauga.messages.common.util.extensions.setVisible import org.prauga.messages.databinding.MenuListItemBinding -import io.reactivex.disposables.CompositeDisposable -import io.reactivex.subjects.PublishSubject -import io.reactivex.subjects.Subject import javax.inject.Inject data class MenuItem(val title: String, val actionId: Int) -class MenuItemAdapter @Inject constructor(private val context: Context, private val colors: Colors) : QkAdapter>() { +class MenuItemAdapter @Inject constructor( + private val context: Context, + private val colors: Colors +) : QkAdapter>() { val menuItemClicks: Subject = PublishSubject.create() @@ -58,15 +61,20 @@ class MenuItemAdapter @Inject constructor(private val context: Context, private val valueInts = if (values != -1) context.resources.getIntArray(values) else null data = context.resources.getStringArray(titles) - .mapIndexed { index, title -> MenuItem(title, valueInts?.getOrNull(index) ?: index) } + .mapIndexed { index, title -> MenuItem(title, valueInts?.getOrNull(index) ?: index) } } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QkBindingViewHolder { - val binding = MenuListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): QkBindingViewHolder { + val binding = + MenuListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) val states = arrayOf( - intArrayOf(android.R.attr.state_activated), - intArrayOf(-android.R.attr.state_activated)) + intArrayOf(android.R.attr.state_activated), + intArrayOf(-android.R.attr.state_activated) + ) val text = parent.context.resolveThemeColor(android.R.attr.textColorTertiary) binding.check.imageTintList = ColorStateList(states, intArrayOf(colors.theme().theme, text)) diff --git a/presentation/src/main/java/com/moez/QKSMS/common/Navigator.kt b/presentation/src/main/java/com/moez/QKSMS/common/Navigator.kt index b97400e29..fd9be9bbb 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/Navigator.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/Navigator.kt @@ -145,8 +145,8 @@ class Navigator @Inject constructor( fun showConversation(threadId: Long, query: String? = null) { val intent = Intent(context, ComposeActivity::class.java) - .putExtra("threadId", threadId) - .putExtra("query", query) + .putExtra("threadId", threadId) + .putExtra("query", query) startActivity(intent) } @@ -188,12 +188,16 @@ class Navigator @Inject constructor( } fun showChangelog() { - val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/danascape/Messages/releases")) + val intent = + Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/danascape/Messages/releases")) startActivityExternal(intent) } fun showLicense() { - val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/danascape/Messages/blob/master/LICENSE")) + val intent = Intent( + Intent.ACTION_VIEW, + Uri.parse("https://github.com/danascape/Messages/blob/master/LICENSE") + ) startActivityExternal(intent) } @@ -215,9 +219,11 @@ class Navigator @Inject constructor( fun showRating() { val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/danascape/Messages")) - .addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY + .addFlags( + Intent.FLAG_ACTIVITY_NO_HISTORY or Intent.FLAG_ACTIVITY_NEW_DOCUMENT - or Intent.FLAG_ACTIVITY_MULTIPLE_TASK) + or Intent.FLAG_ACTIVITY_MULTIPLE_TASK + ) try { startActivityExternal(intent) @@ -240,7 +246,8 @@ class Navigator @Inject constructor( * Launch the Play Store and display the Call Control listing */ fun installCallControl() { - val url = "https://play.google.com/store/apps/details?id=com.flexaspect.android.everycallcontrol" + val url = + "https://play.google.com/store/apps/details?id=com.flexaspect.android.everycallcontrol" val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) startActivityExternal(intent) } @@ -259,56 +266,58 @@ class Navigator @Inject constructor( intent.data = Uri.parse("mailto:") intent.putExtra(Intent.EXTRA_EMAIL, arrayOf("saalim.priv@gmail.com")) intent.putExtra(Intent.EXTRA_SUBJECT, "Messages Support") - intent.putExtra(Intent.EXTRA_TEXT, StringBuilder("\n\n") - .append("\n\n--- Please write your message above this line ---\n\n") - .append("Package: ${context.packageName}\n") - .append("Version: ${BuildConfig.VERSION_NAME}\n") - .append("Device: ${Build.BRAND} ${Build.MODEL}\n") - .append("SDK: ${Build.VERSION.SDK_INT}\n") - .append("Upgraded" - .takeIf { billingManager.upgradeStatus.blockingFirst() } ?: "") - .toString()) + intent.putExtra( + Intent.EXTRA_TEXT, StringBuilder("\n\n") + .append("\n\n--- Please write your message above this line ---\n\n") + .append("Package: ${context.packageName}\n") + .append("Version: ${BuildConfig.VERSION_NAME}\n") + .append("Device: ${Build.BRAND} ${Build.MODEL}\n") + .append("SDK: ${Build.VERSION.SDK_INT}\n") + .append( + "Upgraded" + .takeIf { billingManager.upgradeStatus.blockingFirst() } ?: "") + .toString()) startActivityExternal(intent) } fun showInvite() { Intent(Intent.ACTION_SEND) - .setType("text/plain") - .putExtra(Intent.EXTRA_TEXT, "https://github.com/danascape/Messages/releases/latest") - .let { Intent.createChooser(it, null) } - .let(::startActivityExternal) + .setType("text/plain") + .putExtra(Intent.EXTRA_TEXT, "https://github.com/danascape/Messages/releases/latest") + .let { Intent.createChooser(it, null) } + .let(::startActivityExternal) } fun addContact(address: String) { val intent = Intent(Intent.ACTION_INSERT) - .setType(ContactsContract.Contacts.CONTENT_TYPE) - .putExtra(ContactsContract.Intents.Insert.PHONE, address) + .setType(ContactsContract.Contacts.CONTENT_TYPE) + .putExtra(ContactsContract.Intents.Insert.PHONE, address) startActivityExternal(intent) } fun showContact(lookupKey: String) { val intent = Intent(Intent.ACTION_VIEW) - .setData(Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_LOOKUP_URI, lookupKey)) + .setData(Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_LOOKUP_URI, lookupKey)) startActivityExternal(intent) } fun viewFile(uri: Uri, mimeType: String) { val intent = Intent(Intent.ACTION_VIEW) - .setDataAndType(uri, mimeType.lowercase()) - .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - .let { Intent.createChooser(it, null) } + .setDataAndType(uri, mimeType.lowercase()) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + .let { Intent.createChooser(it, null) } startActivityExternal(intent) } fun shareFile(uri: Uri, mimeType: String) { val intent = Intent(Intent.ACTION_SEND) - .setType(mimeType.lowercase()) - .putExtra(Intent.EXTRA_STREAM, uri) - .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - .let { Intent.createChooser(it, null) } + .setType(mimeType.lowercase()) + .putExtra(Intent.EXTRA_STREAM, uri) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + .let { Intent.createChooser(it, null) } startActivityExternal(intent) } @@ -335,8 +344,8 @@ class Navigator @Inject constructor( val channelId = notificationManager.buildNotificationChannelId(threadId) val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS) - .putExtra(Settings.EXTRA_CHANNEL_ID, channelId) - .putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName) + .putExtra(Settings.EXTRA_CHANNEL_ID, channelId) + .putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName) startActivity(intent) } } @@ -344,7 +353,7 @@ class Navigator @Inject constructor( fun showExactAlarmsSettings() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { val intent = Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM) - .setData(Uri.parse("package:${context.packageName}")) + .setData(Uri.parse("package:${context.packageName}")) startActivity(intent) } } diff --git a/presentation/src/main/java/com/moez/QKSMS/common/QKApplication.kt b/presentation/src/main/java/com/moez/QKSMS/common/QKApplication.kt index e78b83ddc..dc2be8984 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/QKApplication.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/QKApplication.kt @@ -20,10 +20,7 @@ package org.prauga.messages.common import android.app.Activity import android.app.Application -import android.app.Service -import android.content.BroadcastReceiver import android.os.Bundle -import android.view.View import androidx.emoji2.bundled.BundledEmojiCompatConfig import androidx.emoji2.text.EmojiCompat import androidx.work.Configuration @@ -32,10 +29,14 @@ import androidx.work.WorkerFactory import com.moez.QKSMS.manager.SpeakManager import com.uber.rxdogtag.RxDogTag import com.uber.rxdogtag.autodispose.AutoDisposeConfigurer -import dagger.android.AndroidInjection import dagger.android.AndroidInjector import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector +import io.realm.Realm +import io.realm.RealmConfiguration +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch import org.prauga.messages.R import org.prauga.messages.app.utils.AppUtil.applyEdgeToEdgeInsets import org.prauga.messages.common.util.FileLoggingTree @@ -48,11 +49,6 @@ import org.prauga.messages.migration.QkMigration import org.prauga.messages.migration.QkRealmMigration import org.prauga.messages.util.NightModeManager import org.prauga.messages.worker.HousekeepingWorker -import io.realm.Realm -import io.realm.RealmConfiguration -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject diff --git a/presentation/src/main/java/com/moez/QKSMS/common/QkChangeHandler.kt b/presentation/src/main/java/com/moez/QKSMS/common/QkChangeHandler.kt index 35c95d781..ec5a707b5 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/QkChangeHandler.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/QkChangeHandler.kt @@ -45,21 +45,47 @@ class QkChangeHandler : AnimatorChangeHandler(250, true) { if (isPush) { if (from != null) { - animatorSet.play(ObjectAnimator.ofFloat(from, View.TRANSLATION_X, -from.width.toFloat() / 4)) + animatorSet.play( + ObjectAnimator.ofFloat( + from, + View.TRANSLATION_X, + -from.width.toFloat() / 4 + ) + ) } if (to != null) { to.translationZ = 8.dpToPx(to.context).toFloat() - animatorSet.play(ObjectAnimator.ofFloat(to, View.TRANSLATION_X, to.width.toFloat(), 0f)) + animatorSet.play( + ObjectAnimator.ofFloat( + to, + View.TRANSLATION_X, + to.width.toFloat(), + 0f + ) + ) } } else { if (from != null) { from.translationZ = 8.dpToPx(from.context).toFloat() - animatorSet.play(ObjectAnimator.ofFloat(from, View.TRANSLATION_X, from.width.toFloat())) + animatorSet.play( + ObjectAnimator.ofFloat( + from, + View.TRANSLATION_X, + from.width.toFloat() + ) + ) } if (to != null) { // Allow this to have a nice transition when coming off an aborted push animation val fromLeft = from?.translationX ?: 0f - animatorSet.play(ObjectAnimator.ofFloat(to, View.TRANSLATION_X, fromLeft - to.width / 4, 0f)) + animatorSet.play( + ObjectAnimator.ofFloat( + to, + View.TRANSLATION_X, + fromLeft - to.width / 4, + 0f + ) + ) } } diff --git a/presentation/src/main/java/com/moez/QKSMS/common/QkMediaPlayer.kt b/presentation/src/main/java/com/moez/QKSMS/common/QkMediaPlayer.kt index 623bd519d..4c9e0b6b0 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/QkMediaPlayer.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/QkMediaPlayer.kt @@ -14,6 +14,7 @@ object QkMediaPlayer : MediaPlayer() { Playing, Paused } + private var playingState = PlayingState.Stopped override fun start() { diff --git a/presentation/src/main/java/com/moez/QKSMS/common/ViewModelFactory.kt b/presentation/src/main/java/com/moez/QKSMS/common/ViewModelFactory.kt index e9dd81aba..8121161bb 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/ViewModelFactory.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/ViewModelFactory.kt @@ -23,8 +23,10 @@ import androidx.lifecycle.ViewModelProvider import javax.inject.Inject import javax.inject.Provider -class ViewModelFactory @Inject constructor(private val viewModels: MutableMap, Provider>) : ViewModelProvider.Factory { +class ViewModelFactory @Inject constructor(private val viewModels: MutableMap, Provider>) : + ViewModelProvider.Factory { - override fun create(modelClass: Class): T = viewModels[modelClass]?.get() as T + override fun create(modelClass: Class): T = + viewModels[modelClass]?.get() as T } \ No newline at end of file diff --git a/presentation/src/main/java/com/moez/QKSMS/common/androidxcompat/drawerOpen.kt b/presentation/src/main/java/com/moez/QKSMS/common/androidxcompat/drawerOpen.kt index 9cdec1d4d..247ee288a 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/androidxcompat/drawerOpen.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/androidxcompat/drawerOpen.kt @@ -35,7 +35,8 @@ import io.reactivex.functions.Consumer * *Note:* A value will be emitted immediately on subscribe. */ @CheckResult -inline fun DrawerLayout.drawerOpen(gravity: Int): InitialValueObservable = RxDrawerLayout.drawerOpen(this, gravity) +inline fun DrawerLayout.drawerOpen(gravity: Int): InitialValueObservable = + RxDrawerLayout.drawerOpen(this, gravity) /** * An action which sets whether the drawer with `gravity` of `view` is open. @@ -44,4 +45,5 @@ inline fun DrawerLayout.drawerOpen(gravity: Int): InitialValueObservable = RxDrawerLayout.open(this, gravity) \ No newline at end of file +inline fun DrawerLayout.open(gravity: Int): Consumer = + RxDrawerLayout.open(this, gravity) \ No newline at end of file diff --git a/presentation/src/main/java/com/moez/QKSMS/common/base/PvotViewModel.kt b/presentation/src/main/java/com/moez/QKSMS/common/base/PvotViewModel.kt new file mode 100644 index 000000000..55086c80c --- /dev/null +++ b/presentation/src/main/java/com/moez/QKSMS/common/base/PvotViewModel.kt @@ -0,0 +1,42 @@ +package com.moez.QKSMS.common.base + +import androidx.annotation.CallSuper +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.scan +import kotlinx.coroutines.launch +import org.prauga.messages.common.base.QkView + +abstract class PvotViewModel( + initialState: State +) : ViewModel() { + + private val _state = MutableStateFlow(initialState) + val state: StateFlow = _state + + private val reducers = MutableSharedFlow State>() + + init { + viewModelScope.launch { + reducers + .scan(initialState) { current, reducer -> reducer(current) } + .collect { newState -> _state.value = newState } + } + } + + protected fun newState(reducer: State.() -> State) { + viewModelScope.launch { + reducers.emit(reducer) + } + } + + @CallSuper + open fun bindView(view: QkView) { + viewModelScope.launch { + state.collect { view.render(it) } + } + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/moez/QKSMS/common/base/QkActivity.kt b/presentation/src/main/java/com/moez/QKSMS/common/base/QkActivity.kt index bccfe498b..b3360be4e 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/base/QkActivity.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/base/QkActivity.kt @@ -29,11 +29,11 @@ import android.view.WindowManager import android.widget.TextView import androidx.appcompat.widget.Toolbar import androidx.viewbinding.ViewBinding +import io.reactivex.subjects.BehaviorSubject +import io.reactivex.subjects.Subject import org.prauga.messages.R import org.prauga.messages.app.BaseViewBindingActivity import org.prauga.messages.util.Preferences -import io.reactivex.subjects.BehaviorSubject -import io.reactivex.subjects.Subject import javax.inject.Inject abstract class QkActivity( diff --git a/presentation/src/main/java/com/moez/QKSMS/common/base/QkAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/common/base/QkAdapter.kt index d1564c3b5..2daf4f76e 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/base/QkAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/base/QkAdapter.kt @@ -22,9 +22,9 @@ import android.view.View import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView -import org.prauga.messages.common.util.extensions.setVisible import io.reactivex.subjects.BehaviorSubject import io.reactivex.subjects.Subject +import org.prauga.messages.common.util.extensions.setVisible /** * Base RecyclerView.Adapter that provides some convenience when creating a new Adapter, such as @@ -103,10 +103,10 @@ abstract class QkAdapter : RecyclerView.Adapte private fun getDiffUtilCallback(oldData: List, newData: List): DiffUtil.Callback { return object : DiffUtil.Callback() { override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) = - areItemsTheSame(oldData[oldItemPosition], newData[newItemPosition]) + areItemsTheSame(oldData[oldItemPosition], newData[newItemPosition]) override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) = - areContentsTheSame(oldData[oldItemPosition], newData[newItemPosition]) + areContentsTheSame(oldData[oldItemPosition], newData[newItemPosition]) override fun getOldListSize() = oldData.size diff --git a/presentation/src/main/java/com/moez/QKSMS/common/base/QkController.kt b/presentation/src/main/java/com/moez/QKSMS/common/base/QkController.kt index 98eda7050..9f3f89bde 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/base/QkController.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/base/QkController.kt @@ -18,6 +18,7 @@ */ package org.prauga.messages.common.base +import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -27,7 +28,11 @@ import androidx.appcompat.app.AppCompatActivity import com.bluelinelabs.conductor.archlifecycle.LifecycleController import org.prauga.messages.R -abstract class QkController, State, Presenter : QkPresenter> : LifecycleController() { +abstract class QkController< + ViewContract : QkViewContract, + State : Any, + Presenter : QkPresenter> : + LifecycleController() { abstract var presenter: Presenter @@ -42,7 +47,7 @@ abstract class QkController, State, Present @LayoutRes var layoutRes: Int = 0 - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup): View { + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedViewState: Bundle?): View { return inflater.inflate(layoutRes, container, false).also { containerView = it onViewCreated() @@ -73,5 +78,4 @@ abstract class QkController, State, Present super.onDestroy() presenter.onCleared() } - } diff --git a/presentation/src/main/java/com/moez/QKSMS/common/base/QkPresenter.kt b/presentation/src/main/java/com/moez/QKSMS/common/base/QkPresenter.kt index c7337e5b2..972fb416e 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/base/QkPresenter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/base/QkPresenter.kt @@ -20,7 +20,7 @@ package org.prauga.messages.common.base import androidx.annotation.CallSuper import com.uber.autodispose.android.lifecycle.scope -import com.uber.autodispose.autoDisposable +import com.uber.autodispose.autoDispose import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.CompositeDisposable import io.reactivex.rxkotlin.plusAssign @@ -28,7 +28,9 @@ import io.reactivex.subjects.BehaviorSubject import io.reactivex.subjects.PublishSubject import io.reactivex.subjects.Subject -abstract class QkPresenter, State>(initialState: State) { +abstract class QkPresenter( + initialState: State +) where View : QkViewContract { protected val disposables = CompositeDisposable() protected val state: Subject = BehaviorSubject.createDefault(initialState) @@ -39,17 +41,19 @@ abstract class QkPresenter, State>(initialState: St // If we accidentally push a realm object into the state on the wrong thread, switching // to mainThread right here should immediately alert us of the issue disposables += stateReducer - .observeOn(AndroidSchedulers.mainThread()) - .scan(initialState) { state, reducer -> reducer(state) } - .subscribe(state::onNext) + .observeOn(AndroidSchedulers.mainThread()) + .scan(initialState) { state, reducer -> reducer(state) } + .subscribe { newState -> + state.onNext(newState) + } } @CallSuper open fun bindIntents(view: View) { state - .observeOn(AndroidSchedulers.mainThread()) - .autoDisposable(view.scope()) - .subscribe(view::render) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(view.scope()) + .subscribe(view::render) } protected fun newState(reducer: State.() -> State) = stateReducer.onNext(reducer) diff --git a/presentation/src/main/java/com/moez/QKSMS/common/base/QkRealmAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/common/base/QkRealmAdapter.kt index a0eda1893..ec067436b 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/base/QkRealmAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/base/QkRealmAdapter.kt @@ -20,7 +20,6 @@ package org.prauga.messages.common.base import android.view.View import androidx.recyclerview.widget.RecyclerView -import org.prauga.messages.common.util.extensions.setVisible import io.reactivex.subjects.BehaviorSubject import io.reactivex.subjects.Subject import io.realm.OrderedRealmCollection @@ -28,9 +27,11 @@ import io.realm.RealmList import io.realm.RealmModel import io.realm.RealmRecyclerViewAdapter import io.realm.RealmResults +import org.prauga.messages.common.util.extensions.setVisible import timber.log.Timber -abstract class QkRealmAdapter : RealmRecyclerViewAdapter(null, true) { +abstract class QkRealmAdapter : + RealmRecyclerViewAdapter(null, true) { /** * This view can be set, and the adapter will automatically control the visibility of this view @@ -100,7 +101,7 @@ abstract class QkRealmAdapter : RealmRecycler if (needToSelectAll) { for (position in 0 until itemCount) selection += getItemId(position) - } + } // fire a single change event now selectionChanges.onNext(selection) diff --git a/presentation/src/main/java/com/moez/QKSMS/common/base/QkThemedActivity.kt b/presentation/src/main/java/com/moez/QKSMS/common/base/QkThemedActivity.kt index 31181e702..10ae86d08 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/base/QkThemedActivity.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/base/QkThemedActivity.kt @@ -31,7 +31,12 @@ import androidx.core.view.iterator import androidx.lifecycle.Lifecycle import androidx.viewbinding.ViewBinding import com.uber.autodispose.android.lifecycle.scope -import com.uber.autodispose.autoDisposable +import com.uber.autodispose.autoDispose +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.rxkotlin.Observables +import io.reactivex.subjects.BehaviorSubject +import io.reactivex.subjects.Subject import org.prauga.messages.R import org.prauga.messages.common.util.Colors import org.prauga.messages.common.util.extensions.resolveThemeBoolean @@ -42,11 +47,6 @@ import org.prauga.messages.extensions.mapNotNull import org.prauga.messages.repository.ConversationRepository import org.prauga.messages.repository.MessageRepository import org.prauga.messages.util.PhoneNumberUtils -import io.reactivex.Observable -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.rxkotlin.Observables -import io.reactivex.subjects.BehaviorSubject -import io.reactivex.subjects.Subject import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -106,7 +106,7 @@ abstract class QkThemedActivity( .startWith(Optional(conversation.recipients.firstOrNull())) .distinctUntilChanged() } - } + } .switchMap { colors.themeObservable(it.value) } @SuppressLint("InlinedApi") @@ -120,7 +120,7 @@ abstract class QkThemedActivity( Observable.merge(triggers.map { it.asObservable().skip(1) }) .debounce(400, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) - .autoDisposable(scope()) + .autoDispose(scope()) .subscribe { recreate() } // We can only set light nav bar on API 27 in attrs, but we can do it in API 26 here @@ -160,7 +160,7 @@ abstract class QkThemedActivity( menuItem.icon = menuItem.icon?.apply { setTint(tint) } } - }.autoDisposable(scope(Lifecycle.Event.ON_DESTROY)).subscribe() + }.autoDispose(scope(Lifecycle.Event.ON_DESTROY)).subscribe() } open fun getColoredMenuItems(): List { diff --git a/presentation/src/main/java/com/moez/QKSMS/common/base/QkViewContract.kt b/presentation/src/main/java/com/moez/QKSMS/common/base/QkViewContract.kt index 1606fe92a..0e9f31cde 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/base/QkViewContract.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/base/QkViewContract.kt @@ -20,7 +20,7 @@ package org.prauga.messages.common.base import androidx.lifecycle.LifecycleOwner -interface QkViewContract: LifecycleOwner { +interface QkViewContract : LifecycleOwner { fun render(state: State) diff --git a/presentation/src/main/java/com/moez/QKSMS/common/base/QkViewModel.kt b/presentation/src/main/java/com/moez/QKSMS/common/base/QkViewModel.kt index ff036e01c..8c82b5695 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/base/QkViewModel.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/base/QkViewModel.kt @@ -21,7 +21,7 @@ package org.prauga.messages.common.base import androidx.annotation.CallSuper import androidx.lifecycle.ViewModel import com.uber.autodispose.android.lifecycle.scope -import com.uber.autodispose.autoDisposable +import com.uber.autodispose.autoDispose import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.CompositeDisposable import io.reactivex.rxkotlin.plusAssign @@ -29,7 +29,9 @@ import io.reactivex.subjects.BehaviorSubject import io.reactivex.subjects.PublishSubject import io.reactivex.subjects.Subject -abstract class QkViewModel, State>(initialState: State) : ViewModel() { +abstract class QkViewModel( + initialState: State +) : ViewModel() where View : QkView { protected val disposables = CompositeDisposable() protected val state: Subject = BehaviorSubject.createDefault(initialState) @@ -40,21 +42,22 @@ abstract class QkViewModel, State>(initialState: State) // If we accidentally push a realm object into the state on the wrong thread, switching // to mainThread right here should immediately alert us of the issue disposables += stateReducer - .observeOn(AndroidSchedulers.mainThread()) - .scan(initialState) { state, reducer -> reducer(state) } - .subscribe(state::onNext) + .observeOn(AndroidSchedulers.mainThread()) + .scan(initialState) { state, reducer -> reducer(state) } + .subscribe { newState -> + state.onNext(newState) + } } @CallSuper open fun bindView(view: View) { state - .observeOn(AndroidSchedulers.mainThread()) - .autoDisposable(view.scope()) - .subscribe(view::render) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(view.scope()) + .subscribe(view::render) } protected fun newState(reducer: State.() -> State) = stateReducer.onNext(reducer) override fun onCleared() = disposables.dispose() - } \ No newline at end of file diff --git a/presentation/src/main/java/com/moez/QKSMS/common/util/BillingManagerImpl.kt b/presentation/src/main/java/com/moez/QKSMS/common/util/BillingManagerImpl.kt index 851f27158..564fd3401 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/util/BillingManagerImpl.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/util/BillingManagerImpl.kt @@ -1,17 +1,18 @@ package org.prauga.messages.common.util import android.app.Activity -import org.prauga.messages.manager.BillingManager import io.reactivex.Observable import io.reactivex.subjects.BehaviorSubject +import org.prauga.messages.manager.BillingManager import javax.inject.Inject import javax.inject.Singleton @Singleton class BillingManagerImpl @Inject constructor( -): BillingManager { +) : BillingManager { - override val products: Observable> = BehaviorSubject.createDefault(listOf()) + override val products: Observable> = + BehaviorSubject.createDefault(listOf()) override val upgradeStatus: Observable = BehaviorSubject.createDefault(true) override suspend fun checkForPurchases() = Unit diff --git a/presentation/src/main/java/com/moez/QKSMS/common/util/Colors.kt b/presentation/src/main/java/com/moez/QKSMS/common/util/Colors.kt index 0649d70bd..24f088080 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/util/Colors.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/util/Colors.kt @@ -21,12 +21,12 @@ package org.prauga.messages.common.util import android.content.Context import android.graphics.Color import androidx.core.content.res.getColorOrThrow +import io.reactivex.Observable import org.prauga.messages.R import org.prauga.messages.common.util.extensions.getColorCompat import org.prauga.messages.model.Recipient import org.prauga.messages.util.PhoneNumberUtils import org.prauga.messages.util.Preferences -import io.reactivex.Observable import javax.inject.Inject import javax.inject.Singleton import kotlin.math.absoluteValue @@ -64,19 +64,23 @@ class Colors @Inject constructor( R.array.material_deep_orange, R.array.material_brown, R.array.material_gray, - R.array.material_blue_gray) - .map { res -> context.resources.obtainTypedArray(res) } - .map { typedArray -> (0 until typedArray.length()).map(typedArray::getColorOrThrow) } + R.array.material_blue_gray + ) + .map { res -> context.resources.obtainTypedArray(res) } + .map { typedArray -> (0 until typedArray.length()).map(typedArray::getColorOrThrow) } private val randomColors: List = context.resources.obtainTypedArray(R.array.random_colors) - .let { typedArray -> (0 until typedArray.length()).map(typedArray::getColorOrThrow) } + .let { typedArray -> (0 until typedArray.length()).map(typedArray::getColorOrThrow) } private val minimumContrastRatio = 2 // Cache these values so they don't need to be recalculated - private val primaryTextLuminance = measureLuminance(context.getColorCompat(R.color.textPrimaryDark)) - private val secondaryTextLuminance = measureLuminance(context.getColorCompat(R.color.textSecondaryDark)) - private val tertiaryTextLuminance = measureLuminance(context.getColorCompat(R.color.textTertiaryDark)) + private val primaryTextLuminance = + measureLuminance(context.getColorCompat(R.color.textPrimaryDark)) + private val secondaryTextLuminance = + measureLuminance(context.getColorCompat(R.color.textSecondaryDark)) + private val tertiaryTextLuminance = + measureLuminance(context.getColorCompat(R.color.textTertiaryDark)) fun theme(recipient: Recipient? = null): Theme { val pref = prefs.theme(recipient?.id ?: 0) @@ -94,34 +98,34 @@ class Colors @Inject constructor( else -> prefs.theme(recipient.id, generateColor(recipient)) } return pref.asObservable() - .map { color -> Theme(color, this) } + .map { color -> Theme(color, this) } } fun highlightColorForTheme(theme: Int): Int = FloatArray(3) - .apply { Color.colorToHSV(theme, this) } - .let { hsv -> hsv.apply { set(2, 0.75f) } } // 75% value - .let { hsv -> Color.HSVToColor(85, hsv) } // 33% alpha + .apply { Color.colorToHSV(theme, this) } + .let { hsv -> hsv.apply { set(2, 0.75f) } } // 75% value + .let { hsv -> Color.HSVToColor(85, hsv) } // 33% alpha fun textPrimaryOnThemeForColor(color: Int): Int = color - .let { theme -> measureLuminance(theme) } - .let { themeLuminance -> primaryTextLuminance / themeLuminance } - .let { contrastRatio -> contrastRatio < minimumContrastRatio } - .let { contrastRatio -> if (contrastRatio) R.color.textPrimary else R.color.textPrimaryDark } - .let { res -> context.getColorCompat(res) } + .let { theme -> measureLuminance(theme) } + .let { themeLuminance -> primaryTextLuminance / themeLuminance } + .let { contrastRatio -> contrastRatio < minimumContrastRatio } + .let { contrastRatio -> if (contrastRatio) R.color.textPrimary else R.color.textPrimaryDark } + .let { res -> context.getColorCompat(res) } fun textSecondaryOnThemeForColor(color: Int): Int = color - .let { theme -> measureLuminance(theme) } - .let { themeLuminance -> secondaryTextLuminance / themeLuminance } - .let { contrastRatio -> contrastRatio < minimumContrastRatio } - .let { contrastRatio -> if (contrastRatio) R.color.textSecondary else R.color.textSecondaryDark } - .let { res -> context.getColorCompat(res) } + .let { theme -> measureLuminance(theme) } + .let { themeLuminance -> secondaryTextLuminance / themeLuminance } + .let { contrastRatio -> contrastRatio < minimumContrastRatio } + .let { contrastRatio -> if (contrastRatio) R.color.textSecondary else R.color.textSecondaryDark } + .let { res -> context.getColorCompat(res) } fun textTertiaryOnThemeForColor(color: Int): Int = color - .let { theme -> measureLuminance(theme) } - .let { themeLuminance -> tertiaryTextLuminance / themeLuminance } - .let { contrastRatio -> contrastRatio < minimumContrastRatio } - .let { contrastRatio -> if (contrastRatio) R.color.textTertiary else R.color.textTertiaryDark } - .let { res -> context.getColorCompat(res) } + .let { theme -> measureLuminance(theme) } + .let { themeLuminance -> tertiaryTextLuminance / themeLuminance } + .let { contrastRatio -> contrastRatio < minimumContrastRatio } + .let { contrastRatio -> if (contrastRatio) R.color.textTertiary else R.color.textTertiaryDark } + .let { res -> context.getColorCompat(res) } /** * Measures the luminance value of a color to be able to measure the contrast ratio between two materialColors @@ -129,7 +133,7 @@ class Colors @Inject constructor( */ private fun measureLuminance(color: Int): Double { val array = intArrayOf(Color.red(color), Color.green(color), Color.blue(color)) - .map { if (it < 0.03928) it / 12.92 else Math.pow((it + 0.055) / 1.055, 2.4) } + .map { if (it < 0.03928) it / 12.92 else Math.pow((it + 0.055) / 1.055, 2.4) } return 0.2126 * array[0] + 0.7152 * array[1] + 0.0722 * array[2] + 0.05 } diff --git a/presentation/src/main/java/com/moez/QKSMS/common/util/DateFormatter.kt b/presentation/src/main/java/com/moez/QKSMS/common/util/DateFormatter.kt index 2c50d2e3b..200dc82a9 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/util/DateFormatter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/util/DateFormatter.kt @@ -41,9 +41,9 @@ class DateFormatter @Inject constructor(val context: Context) { if (DateFormat.is24HourFormat(context)) { formattedPattern = formattedPattern - .replace("h", "HH") - .replace("K", "HH") - .replace("\\s+a".toRegex(), "") + .replace("h", "HH") + .replace("K", "HH") + .replace("\\s+a".toRegex(), "") } return SimpleDateFormat(formattedPattern, Locale.getDefault()) diff --git a/presentation/src/main/java/com/moez/QKSMS/common/util/FileLoggingTree.kt b/presentation/src/main/java/com/moez/QKSMS/common/util/FileLoggingTree.kt index 62e4706a6..a76f8858d 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/util/FileLoggingTree.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/util/FileLoggingTree.kt @@ -21,9 +21,9 @@ package org.prauga.messages.common.util import android.content.Context import android.net.Uri import android.util.Log +import io.reactivex.schedulers.Schedulers import org.prauga.messages.util.FileUtils import org.prauga.messages.util.Preferences -import io.reactivex.schedulers.Schedulers import timber.log.Timber import java.io.FileNotFoundException import java.text.SimpleDateFormat diff --git a/presentation/src/main/java/com/moez/QKSMS/common/util/LiveRealmData.kt b/presentation/src/main/java/com/moez/QKSMS/common/util/LiveRealmData.kt index 8ce6be989..ae6a42104 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/util/LiveRealmData.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/util/LiveRealmData.kt @@ -23,7 +23,8 @@ import io.realm.RealmChangeListener import io.realm.RealmModel import io.realm.RealmResults -class LiveRealmData(private val results: RealmResults) : LiveData>() { +class LiveRealmData(private val results: RealmResults) : + LiveData>() { private val listener: RealmChangeListener> = RealmChangeListener { results -> value = results diff --git a/presentation/src/main/java/com/moez/QKSMS/common/util/MessageDetailsFormatter.kt b/presentation/src/main/java/com/moez/QKSMS/common/util/MessageDetailsFormatter.kt index 1ff32b2fb..9fb6f1e0a 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/util/MessageDetailsFormatter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/util/MessageDetailsFormatter.kt @@ -36,68 +36,68 @@ class MessageDetailsFormatter @Inject constructor( val builder = StringBuilder() message.type - .takeIf { it.isNotBlank() } - ?.toUpperCase() - ?.let { context.getString(R.string.compose_details_type, it) } - ?.let(builder::appendln) + .takeIf { it.isNotBlank() } + ?.toUpperCase() + ?.let { context.getString(R.string.compose_details_type, it) } + ?.let(builder::appendln) if (message.isSms()) { message.address - .takeIf { it.isNotBlank() && !message.isMe() } - ?.let { context.getString(R.string.compose_details_from, it) } - ?.let(builder::appendln) + .takeIf { it.isNotBlank() && !message.isMe() } + ?.let { context.getString(R.string.compose_details_from, it) } + ?.let(builder::appendln) message.address - .takeIf { it.isNotBlank() && message.isMe() } - ?.let { context.getString(R.string.compose_details_to, it) } - ?.let(builder::appendln) + .takeIf { it.isNotBlank() && message.isMe() } + ?.let { context.getString(R.string.compose_details_to, it) } + ?.let(builder::appendln) } else { val pdu = tryOrNull { PduPersister.getPduPersister(context) - .load(message.getUri()) + .load(message.getUri()) as MultimediaMessagePdu } pdu?.from?.string - ?.takeIf { it.isNotBlank() } - ?.let { context.getString(R.string.compose_details_from, it) } - ?.let(builder::appendln) + ?.takeIf { it.isNotBlank() } + ?.let { context.getString(R.string.compose_details_from, it) } + ?.let(builder::appendln) pdu?.to - ?.let(EncodedStringValue::concat) - ?.takeIf { it.isNotBlank() } - ?.let { context.getString(R.string.compose_details_to, it) } - ?.let(builder::appendln) + ?.let(EncodedStringValue::concat) + ?.takeIf { it.isNotBlank() } + ?.let { context.getString(R.string.compose_details_to, it) } + ?.let(builder::appendln) } message.date - .takeIf { it > 0 && message.isMe() } - ?.let(dateFormatter::getDetailedTimestamp) - ?.let { context.getString(R.string.compose_details_sent, it) } - ?.let(builder::appendln) + .takeIf { it > 0 && message.isMe() } + ?.let(dateFormatter::getDetailedTimestamp) + ?.let { context.getString(R.string.compose_details_sent, it) } + ?.let(builder::appendln) message.dateSent - .takeIf { it > 0 && !message.isMe() } - ?.let(dateFormatter::getDetailedTimestamp) - ?.let { context.getString(R.string.compose_details_sent, it) } - ?.let(builder::appendln) + .takeIf { it > 0 && !message.isMe() } + ?.let(dateFormatter::getDetailedTimestamp) + ?.let { context.getString(R.string.compose_details_sent, it) } + ?.let(builder::appendln) message.date - .takeIf { it > 0 && !message.isMe() } - ?.let(dateFormatter::getDetailedTimestamp) - ?.let { context.getString(R.string.compose_details_received, it) } - ?.let(builder::appendln) + .takeIf { it > 0 && !message.isMe() } + ?.let(dateFormatter::getDetailedTimestamp) + ?.let { context.getString(R.string.compose_details_received, it) } + ?.let(builder::appendln) message.dateSent - .takeIf { it > 0 && message.isMe() } - ?.let(dateFormatter::getDetailedTimestamp) - ?.let { context.getString(R.string.compose_details_delivered, it) } - ?.let(builder::appendln) + .takeIf { it > 0 && message.isMe() } + ?.let(dateFormatter::getDetailedTimestamp) + ?.let { context.getString(R.string.compose_details_delivered, it) } + ?.let(builder::appendln) message.errorCode - .takeIf { it != 0 && message.isSms() } - ?.let { context.getString(R.string.compose_details_error_code, it) } - ?.let(builder::appendln) + .takeIf { it != 0 && message.isSms() } + ?.let { context.getString(R.string.compose_details_error_code, it) } + ?.let(builder::appendln) return builder.toString().trim() } diff --git a/presentation/src/main/java/com/moez/QKSMS/common/util/NotificationManagerImpl.kt b/presentation/src/main/java/com/moez/QKSMS/common/util/NotificationManagerImpl.kt index ced3d2e9e..4fe8b312b 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/util/NotificationManagerImpl.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/util/NotificationManagerImpl.kt @@ -37,6 +37,7 @@ import androidx.core.app.RemoteInput import androidx.core.app.TaskStackBuilder import androidx.core.content.getSystemService import org.prauga.messages.R +import org.prauga.messages.app.receiver.CopyOtpReceiver import org.prauga.messages.common.util.extensions.dpToPx import org.prauga.messages.common.util.extensions.fromRecipient import org.prauga.messages.common.util.extensions.toPerson @@ -47,14 +48,13 @@ import org.prauga.messages.manager.PermissionManager import org.prauga.messages.manager.ShortcutManager import org.prauga.messages.mapper.CursorToPartImpl import org.prauga.messages.receiver.BlockThreadReceiver -import org.prauga.messages.app.receiver.CopyOtpReceiver import org.prauga.messages.receiver.DeleteMessagesReceiver import org.prauga.messages.receiver.MarkArchivedReceiver import org.prauga.messages.receiver.MarkReadReceiver import org.prauga.messages.receiver.MarkSeenReceiver import org.prauga.messages.receiver.RemoteMessagingReceiver -import org.prauga.messages.receiver.SpeakThreadsReceiver import org.prauga.messages.receiver.SendSmsReceiver +import org.prauga.messages.receiver.SpeakThreadsReceiver import org.prauga.messages.repository.ContactRepository import org.prauga.messages.repository.ConversationRepository import org.prauga.messages.repository.MessageRepository @@ -86,7 +86,8 @@ class NotificationManagerImpl @Inject constructor( val VIBRATE_PATTERN = longArrayOf(0, 200, 0, 200) } - private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + private val notificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager init { // Make sure the default channel has been initialized @@ -123,26 +124,38 @@ class NotificationManagerImpl @Inject constructor( } } ?: conversation.recipients.firstOrNull() - val contentIntent = Intent(context, ComposeActivity::class.java).putExtra("threadId", threadId) + val contentIntent = + Intent(context, ComposeActivity::class.java).putExtra("threadId", threadId) val taskStackBuilder = TaskStackBuilder.create(context) - .addParentStack(ComposeActivity::class.java) - .addNextIntent(contentIntent) - val contentPI = taskStackBuilder.getPendingIntent(threadId.toInt(), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + .addParentStack(ComposeActivity::class.java) + .addNextIntent(contentIntent) + val contentPI = taskStackBuilder.getPendingIntent( + threadId.toInt(), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) - val seenIntent = Intent(context, MarkSeenReceiver::class.java).putExtra("threadId", threadId) - val seenPI = PendingIntent.getBroadcast(context, threadId.toInt(), seenIntent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + val seenIntent = + Intent(context, MarkSeenReceiver::class.java).putExtra("threadId", threadId) + val seenPI = PendingIntent.getBroadcast( + context, threadId.toInt(), seenIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) // We can't store a null preference, so map it to a null Uri if the pref string is empty val ringtone = prefs.ringtone(threadId).get() - .takeIf { it.isNotEmpty() } - ?.let(Uri::parse) - ?.also { uri -> - // https://commonsware.com/blog/2016/09/07/notifications-sounds-android-7p0-aggravation.html - context.grantUriPermission("com.android.systemui", uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) - } + .takeIf { it.isNotEmpty() } + ?.let(Uri::parse) + ?.also { uri -> + // https://commonsware.com/blog/2016/09/07/notifications-sounds-android-7p0-aggravation.html + context.grantUriPermission( + "com.android.systemui", + uri, + Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + } - val notification = NotificationCompat.Builder(context, getChannelIdForNotification(threadId)) + val notification = + NotificationCompat.Builder(context, getChannelIdForNotification(threadId)) .setCategory(NotificationCompat.CATEGORY_MESSAGE) .setColor(colors.theme(lastRecipient).theme) .setPriority(NotificationCompat.PRIORITY_MAX) @@ -153,26 +166,28 @@ class NotificationManagerImpl @Inject constructor( .setDeleteIntent(seenPI) .setLights(Color.WHITE, 500, 2000) .setWhen(conversation.lastMessage?.date ?: System.currentTimeMillis()) - .setVibrate(if (prefs.vibration(threadId).get()) VIBRATE_PATTERN else longArrayOf(0)) + .setVibrate( + if (prefs.vibration(threadId).get()) VIBRATE_PATTERN else longArrayOf(0) + ) // if preference set to silence notifications if no recipients in contacts if (prefs.silentNotContact.get() && run { - val msgRecipientNumbers = conversation.recipients.map { it.address } - - // true if any message recipients are in device's contacts - !contactRepo - .getUnmanagedAllContacts() - .flatMap { it.numbers } - .map { it.address } - .any { contactNumber -> - msgRecipientNumbers.any { phoneNumberUtils.compare(contactNumber, it) } - } - }) + val msgRecipientNumbers = conversation.recipients.map { it.address } + + // true if any message recipients are in device's contacts + !contactRepo + .getUnmanagedAllContacts() + .flatMap { it.numbers } + .map { it.address } + .any { contactNumber -> + msgRecipientNumbers.any { phoneNumberUtils.compare(contactNumber, it) } + } + }) notification.setSilent(true) else notification.setSound(ringtone) - // Tell the notification if it's a group message + // Tell the notification if it's a group message val messagingStyle = NotificationCompat.MessagingStyle("Me") if (conversation.recipients.size >= 2) { messagingStyle.isGroupConversation = true @@ -188,13 +203,20 @@ class NotificationManagerImpl @Inject constructor( phoneNumberUtils.compare(recipient.address, message.address) } - if(recipient != null) + if (recipient != null) person.fromRecipient(recipient, context, colors) } - NotificationCompat.MessagingStyle.Message(message.getSummary(), message.date, person.build()).apply { + NotificationCompat.MessagingStyle.Message( + message.getSummary(), + message.date, + person.build() + ).apply { message.parts.firstOrNull { it.isImage() }?.let { part -> - setData(part.type, ContentUris.withAppendedId(CursorToPartImpl.CONTENT_URI, part.id)) + setData( + part.type, + ContentUris.withAppendedId(CursorToPartImpl.CONTENT_URI, part.id) + ) } messagingStyle.addMessage(this) } @@ -208,37 +230,43 @@ class NotificationManagerImpl @Inject constructor( // Set the large icon val avatar = conversation.recipients.takeIf { it.size == 1 } - ?.first()?.contact?.photoUri - ?.let { photoUri -> - GlideApp.with(context) - .asBitmap() - .circleCrop() - .load(photoUri) - .submit(64.dpToPx(context), 64.dpToPx(context)) - } - ?.let { futureGet -> tryOrNull(false) { futureGet.get() } } + ?.first()?.contact?.photoUri + ?.let { photoUri -> + GlideApp.with(context) + .asBitmap() + .circleCrop() + .load(photoUri) + .submit(64.dpToPx(context), 64.dpToPx(context)) + } + ?.let { futureGet -> tryOrNull(false) { futureGet.get() } } // Bind the notification contents based on the notification preview mode when (prefs.notificationPreviews(threadId).get()) { Preferences.NOTIFICATION_PREVIEWS_ALL -> { notification - .setLargeIcon(avatar) - .setStyle(messagingStyle) + .setLargeIcon(avatar) + .setStyle(messagingStyle) } Preferences.NOTIFICATION_PREVIEWS_NAME -> { notification - .setLargeIcon(avatar) - .setContentTitle(conversation.getTitle()) - .setContentText(context.resources.getQuantityString( - R.plurals.notification_new_messages, messages.size, messages.size)) + .setLargeIcon(avatar) + .setContentTitle(conversation.getTitle()) + .setContentText( + context.resources.getQuantityString( + R.plurals.notification_new_messages, messages.size, messages.size + ) + ) } Preferences.NOTIFICATION_PREVIEWS_NONE -> { notification - .setContentTitle(context.getString(R.string.app_name)) - .setContentText(context.resources.getQuantityString( - R.plurals.notification_new_messages, messages.size, messages.size)) + .setContentTitle(context.getString(R.string.app_name)) + .setContentText( + context.resources.getQuantityString( + R.plurals.notification_new_messages, messages.size, messages.size + ) + ) } } @@ -251,80 +279,141 @@ class NotificationManagerImpl @Inject constructor( // Add the action buttons val actionLabels = context.resources.getStringArray(R.array.notification_actions) listOf(prefs.notifAction1, prefs.notifAction2, prefs.notifAction3) - .map { preference -> preference.get() } - .distinct() - .mapNotNull { action -> - when (action) { - Preferences.NOTIFICATION_ACTION_ARCHIVE -> { - val intent = Intent(context, MarkArchivedReceiver::class.java).putExtra("threadId", threadId) - val pi = PendingIntent.getBroadcast(context, threadId.toInt(), intent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) - NotificationCompat.Action.Builder(R.drawable.ic_archive_white_24dp, actionLabels[action], pi) - .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_ARCHIVE).build() - } - - Preferences.NOTIFICATION_ACTION_DELETE -> { - val messageIds = messages.map { it.id }.toLongArray() - val intent = Intent(context, DeleteMessagesReceiver::class.java) - .putExtra("threadId", threadId) - .putExtra("messageIds", messageIds) - val pi = PendingIntent.getBroadcast(context, threadId.toInt(), intent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) - NotificationCompat.Action.Builder(R.drawable.ic_delete_white_24dp, actionLabels[action], pi) - .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_DELETE).build() - } + .map { preference -> preference.get() } + .distinct() + .mapNotNull { action -> + when (action) { + Preferences.NOTIFICATION_ACTION_ARCHIVE -> { + val intent = Intent(context, MarkArchivedReceiver::class.java).putExtra( + "threadId", + threadId + ) + val pi = PendingIntent.getBroadcast( + context, threadId.toInt(), intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + NotificationCompat.Action.Builder( + R.drawable.ic_archive_white_24dp, + actionLabels[action], + pi + ) + .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_ARCHIVE) + .build() + } - Preferences.NOTIFICATION_ACTION_BLOCK -> { - val intent = Intent(context, BlockThreadReceiver::class.java).putExtra("threadId", threadId) - val pi = PendingIntent.getBroadcast(context, threadId.toInt(), intent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) - NotificationCompat.Action.Builder(R.drawable.ic_block_white_24dp, actionLabels[action], pi) - .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MUTE).build() - } + Preferences.NOTIFICATION_ACTION_DELETE -> { + val messageIds = messages.map { it.id }.toLongArray() + val intent = Intent(context, DeleteMessagesReceiver::class.java) + .putExtra("threadId", threadId) + .putExtra("messageIds", messageIds) + val pi = PendingIntent.getBroadcast( + context, threadId.toInt(), intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + NotificationCompat.Action.Builder( + R.drawable.ic_delete_white_24dp, + actionLabels[action], + pi + ) + .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_DELETE) + .build() + } - Preferences.NOTIFICATION_ACTION_READ -> { - val intent = Intent(context, MarkReadReceiver::class.java).putExtra("threadId", threadId) - val pi = PendingIntent.getBroadcast(context, threadId.toInt(), intent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) - NotificationCompat.Action.Builder(R.drawable.ic_check_white_24dp, actionLabels[action], pi) - .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ).build() - } + Preferences.NOTIFICATION_ACTION_BLOCK -> { + val intent = Intent(context, BlockThreadReceiver::class.java).putExtra( + "threadId", + threadId + ) + val pi = PendingIntent.getBroadcast( + context, threadId.toInt(), intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + NotificationCompat.Action.Builder( + R.drawable.ic_block_white_24dp, + actionLabels[action], + pi + ) + .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MUTE) + .build() + } - Preferences.NOTIFICATION_ACTION_REPLY -> { - if (Build.VERSION.SDK_INT >= 24) { - getReplyAction(threadId) - } else { - val intent = Intent(context, QkReplyActivity::class.java).putExtra("threadId", threadId) - val pi = PendingIntent.getActivity(context, threadId.toInt(), intent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) - NotificationCompat.Action - .Builder(R.drawable.ic_reply_white_24dp, actionLabels[action], pi) - .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY).build() - } - } + Preferences.NOTIFICATION_ACTION_READ -> { + val intent = Intent(context, MarkReadReceiver::class.java).putExtra( + "threadId", + threadId + ) + val pi = PendingIntent.getBroadcast( + context, threadId.toInt(), intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + NotificationCompat.Action.Builder( + R.drawable.ic_check_white_24dp, + actionLabels[action], + pi + ) + .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ) + .build() + } - Preferences.NOTIFICATION_ACTION_CALL -> { - val address = conversation.recipients[0]?.address - val intentAction = if (permissions.hasCalling()) Intent.ACTION_CALL else Intent.ACTION_DIAL - val intent = Intent(intentAction, Uri.parse("tel:$address")) - val pi = PendingIntent.getActivity(context, threadId.toInt(), intent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) - NotificationCompat.Action.Builder(R.drawable.ic_call_white_24dp, actionLabels[action], pi) - .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_CALL).build() + Preferences.NOTIFICATION_ACTION_REPLY -> { + if (Build.VERSION.SDK_INT >= 24) { + getReplyAction(threadId) + } else { + val intent = Intent(context, QkReplyActivity::class.java).putExtra( + "threadId", + threadId + ) + val pi = PendingIntent.getActivity( + context, threadId.toInt(), intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + NotificationCompat.Action + .Builder(R.drawable.ic_reply_white_24dp, actionLabels[action], pi) + .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY) + .build() } + } - Preferences.NOTIFICATION_ACTION_SPEAK -> { - val intent = Intent(context, SpeakThreadsReceiver::class.java).putExtra("threadId", threadId) - val pi = PendingIntent.getBroadcast(context, 0, intent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) - NotificationCompat.Action.Builder(R.drawable.ic_speaker_black_24dp, actionLabels[action], pi) - .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_NONE).build() - } + Preferences.NOTIFICATION_ACTION_CALL -> { + val address = conversation.recipients[0]?.address + val intentAction = + if (permissions.hasCalling()) Intent.ACTION_CALL else Intent.ACTION_DIAL + val intent = Intent(intentAction, Uri.parse("tel:$address")) + val pi = PendingIntent.getActivity( + context, threadId.toInt(), intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + NotificationCompat.Action.Builder( + R.drawable.ic_call_white_24dp, + actionLabels[action], + pi + ) + .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_CALL) + .build() + } - else -> null + Preferences.NOTIFICATION_ACTION_SPEAK -> { + val intent = Intent(context, SpeakThreadsReceiver::class.java).putExtra( + "threadId", + threadId + ) + val pi = PendingIntent.getBroadcast( + context, 0, intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + NotificationCompat.Action.Builder( + R.drawable.ic_speaker_black_24dp, + actionLabels[action], + pi + ) + .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_NONE) + .build() } + + else -> null } - .forEach { notification.addAction(it) } + } + .forEach { notification.addAction(it) } // Detect OTP in the latest message and add copy button if found val latestMessage = messages.lastOrNull() @@ -356,8 +445,8 @@ class NotificationManagerImpl @Inject constructor( notification.priority = NotificationCompat.PRIORITY_DEFAULT val intent = Intent(context, QkReplyActivity::class.java) - .putExtra("threadId", threadId) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .putExtra("threadId", threadId) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) context.startActivity(intent) } @@ -369,7 +458,8 @@ class NotificationManagerImpl @Inject constructor( if (prefs.wakeScreen(threadId).get()) { context.getSystemService()?.let { powerManager -> if (!powerManager.isInteractive) { - val flags = PowerManager.SCREEN_DIM_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP + val flags = + PowerManager.SCREEN_DIM_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP val wakeLock = powerManager.newWakeLock(flags, context.packageName) wakeLock.acquire(5000) } @@ -393,11 +483,15 @@ class NotificationManagerImpl @Inject constructor( val threadId = conversation.id - val contentIntent = Intent(context, ComposeActivity::class.java).putExtra("threadId", threadId) + val contentIntent = + Intent(context, ComposeActivity::class.java).putExtra("threadId", threadId) val taskStackBuilder = TaskStackBuilder.create(context) .addParentStack(ComposeActivity::class.java) .addNextIntent(contentIntent) - val contentPI = taskStackBuilder.getPendingIntent(threadId.toInt(), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + val contentPI = taskStackBuilder.getPendingIntent( + threadId.toInt(), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) //Action for resending a failed message val resendIntent = Intent(context, SendSmsReceiver::class.java).apply { @@ -418,9 +512,15 @@ class NotificationManagerImpl @Inject constructor( .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_NONE) .build() - val notification = NotificationCompat.Builder(context, getChannelIdForNotification(threadId)) + val notification = + NotificationCompat.Builder(context, getChannelIdForNotification(threadId)) .setContentTitle(context.getString(R.string.notification_message_failed_title)) - .setContentText(context.getString(R.string.notification_message_failed_text, conversation.getTitle())) + .setContentText( + context.getString( + R.string.notification_message_failed_text, + conversation.getTitle() + ) + ) .addAction(resendAction) .setColor(colors.theme(lastRecipient).theme) .setPriority(NotificationManagerCompat.IMPORTANCE_MAX) @@ -429,30 +529,35 @@ class NotificationManagerImpl @Inject constructor( .setContentIntent(contentPI) .setSound(Uri.parse(prefs.ringtone(threadId).get())) .setLights(Color.WHITE, 500, 2000) - .setVibrate(if (prefs.vibration(threadId).get()) VIBRATE_PATTERN else longArrayOf(0)) + .setVibrate( + if (prefs.vibration(threadId).get()) VIBRATE_PATTERN else longArrayOf(0) + ) notificationManager.notify(threadId.toInt() + 100000, notification.build()) } private fun getReplyAction(threadId: Long): NotificationCompat.Action { - val replyIntent = Intent(context, RemoteMessagingReceiver::class.java).putExtra("threadId", threadId) - val replyPI = PendingIntent.getBroadcast(context, threadId.toInt(), replyIntent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE) + val replyIntent = + Intent(context, RemoteMessagingReceiver::class.java).putExtra("threadId", threadId) + val replyPI = PendingIntent.getBroadcast( + context, threadId.toInt(), replyIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE + ) val title = context.resources.getStringArray(R.array.notification_actions)[ - Preferences.NOTIFICATION_ACTION_REPLY] + Preferences.NOTIFICATION_ACTION_REPLY] val responseSet = context.resources.getStringArray(R.array.qk_responses) val remoteInput = RemoteInput.Builder("body") - .setLabel(title) + .setLabel(title) if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { remoteInput.setChoices(responseSet) } return NotificationCompat.Action.Builder(R.drawable.ic_reply_white_24dp, title, replyPI) - .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY) - .addRemoteInput(remoteInput.build()) - .build() + .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY) + .addRemoteInput(remoteInput.build()) + .build() } /** @@ -467,7 +572,11 @@ class NotificationManagerImpl @Inject constructor( } val channel = when (threadId) { - 0L -> NotificationChannel(DEFAULT_CHANNEL_ID, "Default", NotificationManager.IMPORTANCE_HIGH).apply { + 0L -> NotificationChannel( + DEFAULT_CHANNEL_ID, + "Default", + NotificationManager.IMPORTANCE_HIGH + ).apply { enableLights(true) lightColor = Color.WHITE enableVibration(true) @@ -484,10 +593,12 @@ class NotificationManagerImpl @Inject constructor( enableVibration(true) vibrationPattern = VIBRATE_PATTERN lockscreenVisibility = Notification.VISIBILITY_PUBLIC - setSound(prefs.ringtone().get().let(Uri::parse), AudioAttributes.Builder() + setSound( + prefs.ringtone().get().let(Uri::parse), AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_NOTIFICATION) .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) - .build()) + .build() + ) } } } @@ -503,7 +614,7 @@ class NotificationManagerImpl @Inject constructor( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { return notificationManager.notificationChannels - .find { channel -> channel.id == channelId } + .find { channel -> channel.id == channelId } } return null @@ -543,15 +654,15 @@ class NotificationManagerImpl @Inject constructor( } return NotificationCompat.Builder(context, BACKUP_RESTORE_CHANNEL_ID) - .setContentTitle(context.getString(R.string.backup_restoring)) - .setShowWhen(false) - .setWhen(System.currentTimeMillis()) // Set this anyway in case it's shown - .setSmallIcon(R.drawable.ic_file_download_black_24dp) - .setColor(colors.theme().theme) - .setCategory(NotificationCompat.CATEGORY_PROGRESS) - .setPriority(NotificationCompat.PRIORITY_MIN) - .setProgress(0, 0, true) - .setOngoing(true) + .setContentTitle(context.getString(R.string.backup_restoring)) + .setShowWhen(false) + .setWhen(System.currentTimeMillis()) // Set this anyway in case it's shown + .setSmallIcon(R.drawable.ic_file_download_black_24dp) + .setColor(colors.theme().theme) + .setCategory(NotificationCompat.CATEGORY_PROGRESS) + .setPriority(NotificationCompat.PRIORITY_MIN) + .setProgress(0, 0, true) + .setOngoing(true) } override fun cancel(i: Int) { diff --git a/presentation/src/main/java/com/moez/QKSMS/common/util/QkActivityResultContracts.kt b/presentation/src/main/java/com/moez/QKSMS/common/util/QkActivityResultContracts.kt index b4319035d..971c2a51a 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/util/QkActivityResultContracts.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/util/QkActivityResultContracts.kt @@ -18,8 +18,8 @@ class QkActivityResultContracts { class OpenDocument : ActivityResultContract() { override fun createIntent(context: Context, input: OpenDocumentParams): Intent { val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) - .putExtra(Intent.EXTRA_MIME_TYPES, input.mimeTypes.toTypedArray()) - .setType("*/*") + .putExtra(Intent.EXTRA_MIME_TYPES, input.mimeTypes.toTypedArray()) + .setType("*/*") if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, input.initialUri) diff --git a/presentation/src/main/java/com/moez/QKSMS/common/util/QkChooserTargetService.kt b/presentation/src/main/java/com/moez/QKSMS/common/util/QkChooserTargetService.kt index 5bed139ee..d94d31245 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/util/QkChooserTargetService.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/util/QkChooserTargetService.kt @@ -35,17 +35,21 @@ import javax.inject.Inject class QkChooserTargetService : ChooserTargetService() { - @Inject lateinit var conversationRepo: ConversationRepository + @Inject + lateinit var conversationRepo: ConversationRepository override fun onCreate() { appComponent.inject(this) super.onCreate() } - override fun onGetChooserTargets(targetActivityName: ComponentName?, matchedFilter: IntentFilter?): List { + override fun onGetChooserTargets( + targetActivityName: ComponentName?, + matchedFilter: IntentFilter? + ): List { return conversationRepo.getTopConversations() - .take(3) - .map(this::createShortcutForConversation) + .take(3) + .map(this::createShortcutForConversation) } private fun createShortcutForConversation(conversation: Conversation): ChooserTarget { @@ -53,10 +57,10 @@ class QkChooserTargetService : ChooserTargetService() { 1 -> { val photoUri = conversation.recipients.first()?.contact?.photoUri val request = GlideApp.with(this) - .asBitmap() - .circleCrop() - .load(photoUri) - .submit() + .asBitmap() + .circleCrop() + .load(photoUri) + .submit() val bitmap = tryOrNull(false) { request.get() } if (bitmap != null) Icon.createWithBitmap(bitmap) @@ -68,7 +72,13 @@ class QkChooserTargetService : ChooserTargetService() { val componentName = ComponentName(this, ComposeActivity::class.java) - return ChooserTarget(conversation.getTitle(), icon, 1f, componentName, bundleOf("threadId" to conversation.id)) + return ChooserTarget( + conversation.getTitle(), + icon, + 1f, + componentName, + bundleOf("threadId" to conversation.id) + ) } } \ No newline at end of file diff --git a/presentation/src/main/java/com/moez/QKSMS/common/util/ShortcutManagerImpl.kt b/presentation/src/main/java/com/moez/QKSMS/common/util/ShortcutManagerImpl.kt index 1c5ed5785..358adfd60 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/util/ShortcutManagerImpl.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/util/ShortcutManagerImpl.kt @@ -26,13 +26,13 @@ import android.os.Build import androidx.core.app.Person import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutManagerCompat +import me.leolin.shortcutbadger.ShortcutBadger import org.prauga.messages.common.util.extensions.getThemedIcon import org.prauga.messages.common.util.extensions.toPerson import org.prauga.messages.feature.compose.ComposeActivity import org.prauga.messages.model.Conversation import org.prauga.messages.repository.ConversationRepository import org.prauga.messages.repository.MessageRepository -import me.leolin.shortcutbadger.ShortcutBadger import timber.log.Timber import javax.inject.Inject @@ -50,12 +50,13 @@ class ShortcutManagerImpl @Inject constructor( override fun updateShortcuts() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { - val shortcutManager = context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager + val shortcutManager = + context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager if (shortcutManager.isRateLimitingActive) return val shortcuts: List = conversationRepo.getTopConversations() - .take(shortcutManager.maxShortcutCountPerActivity - shortcutManager.manifestShortcuts.size) - .map { conversation -> createShortcutForConversation(conversation) } + .take(shortcutManager.maxShortcutCountPerActivity - shortcutManager.manifestShortcuts.size) + .map { conversation -> createShortcutForConversation(conversation) } ShortcutManagerCompat.setDynamicShortcuts(context, shortcuts) } @@ -67,14 +68,14 @@ class ShortcutManagerImpl @Inject constructor( override fun getShortcut(threadId: Long): ShortcutInfoCompat? { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { var sc = getShortcuts().find { it.id == threadId.toString() } - if(sc != null) + if (sc != null) sc = updateShortcut(sc) - if (sc == null) { + if (sc == null) { val conv = conversationRepo.getConversation(threadId) if (conv == null) return null else - sc = createShortcutForConversation(conv) + sc = createShortcutForConversation(conv) } return sc } else { @@ -87,8 +88,9 @@ class ShortcutManagerImpl @Inject constructor( */ override fun reportShortcutUsed(threadId: Long) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { - val shortcutManager = context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager - if(getShortcut(threadId ) == null) { + val shortcutManager = + context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager + if (getShortcut(threadId) == null) { val conversation = conversationRepo.getOrCreateConversation(threadId) if (conversation == null) return @@ -105,7 +107,8 @@ class ShortcutManagerImpl @Inject constructor( val icon = when { conversation.recipients.size == 1 -> { val recipient = conversation.recipients.first()!! - recipient.getThemedIcon(context, + recipient.getThemedIcon( + context, colors.theme(recipient), ShortcutManagerCompat.getIconMaxWidth(context), ShortcutManagerCompat.getIconMaxHeight(context) @@ -113,28 +116,30 @@ class ShortcutManagerImpl @Inject constructor( } else -> { - conversation.getThemedIcon(context, + conversation.getThemedIcon( + context, ShortcutManagerCompat.getIconMaxWidth(context), ShortcutManagerCompat.getIconMaxHeight(context) ) } } - val persons: Array = conversation.recipients.map { it -> it.toPerson(context, colors) }.toTypedArray(); + val persons: Array = + conversation.recipients.map { it -> it.toPerson(context, colors) }.toTypedArray(); val intent = Intent(context, ComposeActivity::class.java) - .setAction(Intent.ACTION_VIEW) - .putExtra("threadId", conversation.id) - .putExtra("fromShortcut", true) + .setAction(Intent.ACTION_VIEW) + .putExtra("threadId", conversation.id) + .putExtra("fromShortcut", true) val sc = ShortcutInfoCompat.Builder(context, "${conversation.id}") - .setShortLabel(conversation.getTitle()) - .setLongLabel(conversation.getTitle()) - .setIcon(icon) - .setIntent(intent) - .setPersons(persons) - .setLongLived(true) - .build() + .setShortLabel(conversation.getTitle()) + .setLongLabel(conversation.getTitle()) + .setIcon(icon) + .setIntent(intent) + .setPersons(persons) + .setLongLived(true) + .build() ShortcutManagerCompat.pushDynamicShortcut(context, sc) return sc @@ -151,7 +156,7 @@ class ShortcutManagerImpl @Inject constructor( } } - private fun getShortcuts() : List { + private fun getShortcuts(): List { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { return ShortcutManagerCompat.getDynamicShortcuts(context) } else { diff --git a/presentation/src/main/java/com/moez/QKSMS/common/util/TextViewStyler.kt b/presentation/src/main/java/com/moez/QKSMS/common/util/TextViewStyler.kt index d98386736..74f856f54 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/util/TextViewStyler.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/util/TextViewStyler.kt @@ -35,7 +35,6 @@ import org.prauga.messages.util.Preferences import javax.inject.Inject - class TextViewStyler @Inject constructor( private val prefs: Preferences, private val colors: Colors, @@ -61,27 +60,31 @@ class TextViewStyler @Inject constructor( var textSizeAttr = 0 when (this) { - is QkTextView -> context.obtainStyledAttributes(attrs, R.styleable.QkTextView)?.run { - colorAttr = getInt(R.styleable.QkTextView_textColor, -1) - textSizeAttr = getInt(R.styleable.QkTextView_textSize, -1) - recycle() - } - - is QkEditText -> context.obtainStyledAttributes(attrs, R.styleable.QkEditText)?.run { - colorAttr = getInt(R.styleable.QkEditText_textColor, -1) - textSizeAttr = getInt(R.styleable.QkEditText_textSize, -1) - recycle() - } + is QkTextView -> context.obtainStyledAttributes(attrs, R.styleable.QkTextView) + ?.run { + colorAttr = getInt(R.styleable.QkTextView_textColor, -1) + textSizeAttr = getInt(R.styleable.QkTextView_textSize, -1) + recycle() + } + + is QkEditText -> context.obtainStyledAttributes(attrs, R.styleable.QkEditText) + ?.run { + colorAttr = getInt(R.styleable.QkEditText_textColor, -1) + textSizeAttr = getInt(R.styleable.QkEditText_textSize, -1) + recycle() + } else -> return } - setTextColor(when (colorAttr) { - COLOR_PRIMARY_ON_THEME -> context.getColorCompat(R.color.textPrimaryDark) - COLOR_SECONDARY_ON_THEME -> context.getColorCompat(R.color.textSecondaryDark) - COLOR_TERTIARY_ON_THEME -> context.getColorCompat(R.color.textTertiaryDark) - COLOR_THEME -> context.getColorCompat(R.color.tools_theme) - else -> currentTextColor - }) + setTextColor( + when (colorAttr) { + COLOR_PRIMARY_ON_THEME -> context.getColorCompat(R.color.textPrimaryDark) + COLOR_SECONDARY_ON_THEME -> context.getColorCompat(R.color.textSecondaryDark) + COLOR_TERTIARY_ON_THEME -> context.getColorCompat(R.color.textTertiaryDark) + COLOR_THEME -> context.getColorCompat(R.color.tools_theme) + else -> currentTextColor + } + ) textSize = when (textSizeAttr) { SIZE_PRIMARY -> 16f @@ -107,17 +110,19 @@ class TextViewStyler @Inject constructor( } when (textView) { - is QkTextView -> textView.context.obtainStyledAttributes(attrs, R.styleable.QkTextView).run { - colorAttr = getInt(R.styleable.QkTextView_textColor, -1) - textSizeAttr = getInt(R.styleable.QkTextView_textSize, -1) - recycle() - } + is QkTextView -> textView.context.obtainStyledAttributes(attrs, R.styleable.QkTextView) + .run { + colorAttr = getInt(R.styleable.QkTextView_textColor, -1) + textSizeAttr = getInt(R.styleable.QkTextView_textSize, -1) + recycle() + } - is QkEditText -> textView.context.obtainStyledAttributes(attrs, R.styleable.QkEditText).run { - colorAttr = getInt(R.styleable.QkEditText_textColor, -1) - textSizeAttr = getInt(R.styleable.QkEditText_textSize, -1) - recycle() - } + is QkEditText -> textView.context.obtainStyledAttributes(attrs, R.styleable.QkEditText) + .run { + colorAttr = getInt(R.styleable.QkEditText_textColor, -1) + textSizeAttr = getInt(R.styleable.QkEditText_textSize, -1) + recycle() + } else -> return } @@ -132,7 +137,8 @@ class TextViewStyler @Inject constructor( setTextSize(textView, textSizeAttr) if (textView is EditText) { - val drawable = textView.resources.getDrawable(R.drawable.cursor).apply { setTint(colors.theme().theme) } + val drawable = textView.resources.getDrawable(R.drawable.cursor) + .apply { setTint(colors.theme().theme) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { textView.textCursorDrawable = drawable } diff --git a/presentation/src/main/java/com/moez/QKSMS/common/util/extensions/CalendarExtensions.kt b/presentation/src/main/java/com/moez/QKSMS/common/util/extensions/CalendarExtensions.kt index 87cd0b9f2..8fc2a93d0 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/util/extensions/CalendarExtensions.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/util/extensions/CalendarExtensions.kt @@ -21,11 +21,15 @@ package org.prauga.messages.common.util.extensions import java.util.Calendar fun Calendar.isSameDay(other: Calendar): Boolean { - return get(Calendar.YEAR) == other.get(Calendar.YEAR) && get(Calendar.DAY_OF_YEAR) == other.get(Calendar.DAY_OF_YEAR) + return get(Calendar.YEAR) == other.get(Calendar.YEAR) && get(Calendar.DAY_OF_YEAR) == other.get( + Calendar.DAY_OF_YEAR + ) } fun Calendar.isSameWeek(other: Calendar): Boolean { - return get(Calendar.YEAR) == other.get(Calendar.YEAR) && get(Calendar.WEEK_OF_YEAR) == other.get(Calendar.WEEK_OF_YEAR) + return get(Calendar.YEAR) == other.get(Calendar.YEAR) && get(Calendar.WEEK_OF_YEAR) == other.get( + Calendar.WEEK_OF_YEAR + ) } fun Calendar.isSameYear(other: Calendar): Boolean { diff --git a/presentation/src/main/java/com/moez/QKSMS/common/util/extensions/DialogExtensions.kt b/presentation/src/main/java/com/moez/QKSMS/common/util/extensions/DialogExtensions.kt index 2e413d8b5..b7649fe61 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/util/extensions/DialogExtensions.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/util/extensions/DialogExtensions.kt @@ -22,15 +22,24 @@ import androidx.annotation.StringRes import androidx.appcompat.app.AlertDialog import io.reactivex.subjects.Subject -fun AlertDialog.Builder.setPositiveButton(@StringRes textId: Int, subject: Subject): AlertDialog.Builder { +fun AlertDialog.Builder.setPositiveButton( + @StringRes textId: Int, + subject: Subject +): AlertDialog.Builder { return setPositiveButton(textId) { _, _ -> subject.onNext(Unit) } } -fun AlertDialog.Builder.setNegativeButton(@StringRes textId: Int, subject: Subject): AlertDialog.Builder { +fun AlertDialog.Builder.setNegativeButton( + @StringRes textId: Int, + subject: Subject +): AlertDialog.Builder { return setNegativeButton(textId) { _, _ -> subject.onNext(Unit) } } -fun AlertDialog.Builder.setNeutralButton(@StringRes textId: Int, subject: Subject): AlertDialog.Builder { +fun AlertDialog.Builder.setNeutralButton( + @StringRes textId: Int, + subject: Subject +): AlertDialog.Builder { return setNeutralButton(textId) { _, _ -> subject.onNext(Unit) } } diff --git a/presentation/src/main/java/com/moez/QKSMS/common/util/extensions/NumberExtensions.kt b/presentation/src/main/java/com/moez/QKSMS/common/util/extensions/NumberExtensions.kt index cf690c563..ee7da673b 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/util/extensions/NumberExtensions.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/util/extensions/NumberExtensions.kt @@ -23,7 +23,11 @@ import android.graphics.Color import android.util.TypedValue fun Int.dpToPx(context: Context): Int { - return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), context.resources.displayMetrics).toInt() + return TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + this.toFloat(), + context.resources.displayMetrics + ).toInt() } fun Int.withAlpha(alpha: Int): Int { diff --git a/presentation/src/main/java/com/moez/QKSMS/common/util/extensions/ProgressExtensions.kt b/presentation/src/main/java/com/moez/QKSMS/common/util/extensions/ProgressExtensions.kt index 536bb54f4..2cd50dca6 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/util/extensions/ProgressExtensions.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/util/extensions/ProgressExtensions.kt @@ -25,7 +25,12 @@ import org.prauga.messages.repository.BackupRepository fun BackupRepository.Progress.getLabel(context: Context): String? { return when (this) { is BackupRepository.Progress.Parsing -> context.getString(R.string.backup_progress_parsing) - is BackupRepository.Progress.Running -> context.getString(R.string.backup_progress_running, count, max) + is BackupRepository.Progress.Running -> context.getString( + R.string.backup_progress_running, + count, + max + ) + is BackupRepository.Progress.Saving -> context.getString(R.string.backup_progress_saving) is BackupRepository.Progress.Syncing -> context.getString(R.string.backup_progress_syncing) is BackupRepository.Progress.Finished -> context.getString(R.string.backup_progress_finished) diff --git a/presentation/src/main/java/com/moez/QKSMS/common/util/extensions/RecipientExtensions.kt b/presentation/src/main/java/com/moez/QKSMS/common/util/extensions/RecipientExtensions.kt index 3bb9d0fa0..87db3b12e 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/util/extensions/RecipientExtensions.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/util/extensions/RecipientExtensions.kt @@ -26,9 +26,14 @@ import org.prauga.messages.util.tryOrNull import timber.log.Timber -fun Recipient.getThemedIcon(context: Context, theme: Colors.Theme, width: Int, height: Int): IconCompat { - var icon : IconCompat? = null - if(contact != null) { +fun Recipient.getThemedIcon( + context: Context, + theme: Colors.Theme, + width: Int, + height: Int +): IconCompat { + var icon: IconCompat? = null + if (contact != null) { val req = GlideApp.with(context) .asBitmap() .circleCrop() @@ -41,7 +46,7 @@ fun Recipient.getThemedIcon(context: Context, theme: Colors.Theme, width: Int, h else icon = IconCompat.createWithBitmap(bitmap) } - if(icon == null) { + if (icon == null) { // If there is no contact or no photo, create the default icon using the avatar_view layout try { val inflater = LayoutInflater.from(context) @@ -56,12 +61,17 @@ fun Recipient.getThemedIcon(context: Context, theme: Colors.Theme, width: Int, h view.setBackgroundColor(theme.theme) view.setBackgroundTint(theme.theme) textView.setTextColor(theme.textPrimary) - TextViewCompat.setAutoSizeTextTypeWithDefaults(textView, TextViewCompat.AUTO_SIZE_TEXT_TYPE_NONE) + TextViewCompat.setAutoSizeTextTypeWithDefaults( + textView, + TextViewCompat.AUTO_SIZE_TEXT_TYPE_NONE + ) textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, height * 0.5f) - iconView.layoutParams = FrameLayout.LayoutParams((width * 0.5).toInt(), (height * 0.5).toInt(), - Gravity.CENTER) + iconView.layoutParams = FrameLayout.LayoutParams( + (width * 0.5).toInt(), (height * 0.5).toInt(), + Gravity.CENTER + ) - if(contact != null) { + if (contact != null) { val initials = contact!!.name .substringBefore(',') .split(" ") @@ -77,8 +87,7 @@ fun Recipient.getThemedIcon(context: Context, theme: Colors.Theme, width: Int, h textView.text = null iconView.visibility = VISIBLE } - } - else { + } else { textView.visibility = GONE iconView.visibility = VISIBLE iconView.setTint(theme.textPrimary) @@ -92,7 +101,11 @@ fun Recipient.getThemedIcon(context: Context, theme: Colors.Theme, width: Int, h layout(0, 0, measuredWidth, measuredHeight) } - val bitmap = createBitmap(container.measuredWidth, container.measuredHeight, Bitmap.Config.ARGB_8888) + val bitmap = createBitmap( + container.measuredWidth, + container.measuredHeight, + Bitmap.Config.ARGB_8888 + ) val canvas = android.graphics.Canvas(bitmap) canvas.clipPath(Path().apply { addCircle( @@ -105,13 +118,12 @@ fun Recipient.getThemedIcon(context: Context, theme: Colors.Theme, width: Int, h container.draw(canvas) icon = IconCompat.createWithBitmap(bitmap) - } - catch (e: Exception) { + } catch (e: Exception) { Timber.e(e) return IconCompat.createWithResource(context, R.mipmap.ic_shortcut_person) } } - return icon + return icon ?: IconCompat.createWithResource(context, R.mipmap.ic_shortcut_person) } @TargetApi(29) @@ -136,7 +148,7 @@ fun Person.Builder.fromRecipient( * Return a Person object corresponding to this recipient */ @TargetApi(29) -fun Recipient.toPerson(context : Context, colors: Colors): Person { +fun Recipient.toPerson(context: Context, colors: Colors): Person { val person = Person.Builder().fromRecipient(this, context, colors) return person.build() } diff --git a/presentation/src/main/java/com/moez/QKSMS/common/util/extensions/VCardExtension.kt b/presentation/src/main/java/com/moez/QKSMS/common/util/extensions/VCardExtension.kt index 759f17275..a0af021d8 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/util/extensions/VCardExtension.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/util/extensions/VCardExtension.kt @@ -23,6 +23,6 @@ import ezvcard.VCard fun VCard.getDisplayName(): String? { return formattedName?.value - ?: telephoneNumbers?.firstOrNull()?.text - ?: emails?.firstOrNull()?.value + ?: telephoneNumbers?.firstOrNull()?.text + ?: emails?.firstOrNull()?.value } diff --git a/presentation/src/main/java/com/moez/QKSMS/common/util/extensions/ViewExtensions.kt b/presentation/src/main/java/com/moez/QKSMS/common/util/extensions/ViewExtensions.kt index f04263bb4..898b365f0 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/util/extensions/ViewExtensions.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/util/extensions/ViewExtensions.kt @@ -84,7 +84,12 @@ fun View.setBackgroundTint(color: Int?) { } fun View.setPadding(left: Int? = null, top: Int? = null, right: Int? = null, bottom: Int? = null) { - setPadding(left ?: paddingLeft, top ?: paddingTop, right ?: paddingRight, bottom ?: paddingBottom) + setPadding( + left ?: paddingLeft, + top ?: paddingTop, + right ?: paddingRight, + bottom ?: paddingBottom + ) } fun View.setVisible(visible: Boolean, invisible: Int = View.GONE) { diff --git a/presentation/src/main/java/com/moez/QKSMS/common/widget/AvatarView.kt b/presentation/src/main/java/com/moez/QKSMS/common/widget/AvatarView.kt index 93de0bdbb..522570d94 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/widget/AvatarView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/widget/AvatarView.kt @@ -26,7 +26,6 @@ import org.prauga.messages.R import org.prauga.messages.common.Navigator import org.prauga.messages.common.util.Colors import org.prauga.messages.common.util.extensions.getColorCompat -import org.prauga.messages.common.util.extensions.setBackgroundTint import org.prauga.messages.common.util.extensions.setTint import org.prauga.messages.databinding.AvatarViewBinding import org.prauga.messages.injection.appComponent @@ -38,8 +37,10 @@ class AvatarView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null ) : FrameLayout(context, attrs) { - @Inject lateinit var colors: Colors - @Inject lateinit var navigator: Navigator + @Inject + lateinit var colors: Colors + @Inject + lateinit var navigator: Navigator private var lookupKey: String? = null private var fullName: String? = null @@ -86,15 +87,16 @@ class AvatarView @JvmOverloads constructor( layout.icon.setTint(context.getColorCompat(R.color.avatar_icon_color)) val initials = fullName - ?.substringBefore(',') - ?.split(" ").orEmpty() - .filter { name -> name.isNotEmpty() } - .map { name -> name[0] } - .filter { initial -> initial.isLetterOrDigit() } - .map { initial -> initial.toString() } + ?.substringBefore(',') + ?.split(" ").orEmpty() + .filter { name -> name.isNotEmpty() } + .map { name -> name[0] } + .filter { initial -> initial.isLetterOrDigit() } + .map { initial -> initial.toString() } if (initials.isNotEmpty()) { - layout.initial.text = if (initials.size > 1) initials.first() + initials.last() else initials.first() + layout.initial.text = + if (initials.size > 1) initials.first() + initials.last() else initials.first() layout.icon.visibility = GONE } else { layout.initial.text = null @@ -104,8 +106,8 @@ class AvatarView @JvmOverloads constructor( layout.photo.setImageDrawable(null) photoUri?.let { photoUri -> GlideApp.with(layout.photo) - .load(photoUri) - .into(layout.photo) + .load(photoUri) + .into(layout.photo) } } } diff --git a/presentation/src/main/java/com/moez/QKSMS/common/widget/BubbleImageView.kt b/presentation/src/main/java/com/moez/QKSMS/common/widget/BubbleImageView.kt index 71eeea328..774bc5b54 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/widget/BubbleImageView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/widget/BubbleImageView.kt @@ -26,9 +26,15 @@ import android.util.AttributeSet import android.widget.ImageView import org.prauga.messages.common.util.extensions.dpToPx -class BubbleImageView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : ImageView(context, attrs) { +class BubbleImageView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : + ImageView(context, attrs) { - enum class Style(val topLeft: Boolean, val topRight: Boolean, val bottomRight: Boolean, val bottomLeft: Boolean) { + enum class Style( + val topLeft: Boolean, + val topRight: Boolean, + val bottomRight: Boolean, + val bottomLeft: Boolean + ) { ONLY(true, true, true, true), IN_FIRST(true, true, true, false), @@ -69,8 +75,10 @@ class BubbleImageView @JvmOverloads constructor(context: Context, attrs: Attribu val width = width.toFloat() val height = height.toFloat() - val cornerRectSmall = RectF().apply { set(-radiusSmall, -radiusSmall, radiusSmall, radiusSmall) } - val cornerRectLarge = RectF().apply { set(-radiusLarge, -radiusLarge, radiusLarge, radiusLarge) } + val cornerRectSmall = + RectF().apply { set(-radiusSmall, -radiusSmall, radiusSmall, radiusSmall) } + val cornerRectLarge = + RectF().apply { set(-radiusLarge, -radiusLarge, radiusLarge, radiusLarge) } if (bubbleStyle.topLeft) { cornerRectLarge.offsetTo(0f, 0f) diff --git a/presentation/src/main/java/com/moez/QKSMS/common/widget/GroupAvatarView.kt b/presentation/src/main/java/com/moez/QKSMS/common/widget/GroupAvatarView.kt index 752d62f79..d913109c2 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/widget/GroupAvatarView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/widget/GroupAvatarView.kt @@ -52,10 +52,12 @@ class GroupAvatarView @JvmOverloads constructor( } private fun updateView() { - layout.avatar1Frame.setBackgroundTint(when (recipients.size > 1) { - true -> context.resolveThemeColor(android.R.attr.windowBackground) - false -> context.getColorCompat(android.R.color.transparent) - }) + layout.avatar1Frame.setBackgroundTint( + when (recipients.size > 1) { + true -> context.resolveThemeColor(android.R.attr.windowBackground) + false -> context.getColorCompat(android.R.color.transparent) + } + ) layout.avatar1Frame.updateLayoutParams { matchConstraintPercentWidth = if (recipients.size > 1) 0.75f else 1.0f } diff --git a/presentation/src/main/java/com/moez/QKSMS/common/widget/MicInputCloudView.kt b/presentation/src/main/java/com/moez/QKSMS/common/widget/MicInputCloudView.kt index 07520cb32..5ef2e3e9b 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/widget/MicInputCloudView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/widget/MicInputCloudView.kt @@ -351,7 +351,7 @@ class MicInputCloudView(context: Context, attrs: AttributeSet) : View(context, a invalidate() } - fun getState() : ViewState { + fun getState(): ViewState { return state } diff --git a/presentation/src/main/java/com/moez/QKSMS/common/widget/PagerTitleView.kt b/presentation/src/main/java/com/moez/QKSMS/common/widget/PagerTitleView.kt index 709a951d1..e46212422 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/widget/PagerTitleView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/widget/PagerTitleView.kt @@ -18,6 +18,7 @@ */ package org.prauga.messages.common.widget +import android.R import android.content.Context import android.content.res.ColorStateList import android.util.AttributeSet @@ -26,7 +27,9 @@ import android.widget.LinearLayout import android.widget.TextView import androidx.viewpager.widget.ViewPager import com.uber.autodispose.android.ViewScopeProvider -import com.uber.autodispose.autoDisposable +import com.uber.autodispose.autoDispose +import io.reactivex.subjects.BehaviorSubject +import io.reactivex.subjects.Subject import org.prauga.messages.common.util.Colors import org.prauga.messages.common.util.extensions.forEach import org.prauga.messages.common.util.extensions.resolveThemeColor @@ -34,14 +37,15 @@ import org.prauga.messages.databinding.TabViewBinding import org.prauga.messages.extensions.Optional import org.prauga.messages.injection.appComponent import org.prauga.messages.repository.ConversationRepository -import io.reactivex.subjects.BehaviorSubject -import io.reactivex.subjects.Subject import javax.inject.Inject -class PagerTitleView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : LinearLayout(context, attrs) { +class PagerTitleView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : + LinearLayout(context, attrs) { - @Inject lateinit var colors: Colors - @Inject lateinit var conversationRepo: ConversationRepository + @Inject + lateinit var colors: Colors + @Inject + lateinit var conversationRepo: ConversationRepository private val recipientId: Subject = BehaviorSubject.create() @@ -89,23 +93,24 @@ class PagerTitleView @JvmOverloads constructor(context: Context, attrs: Attribut super.onAttachedToWindow() val states = arrayOf( - intArrayOf(android.R.attr.state_activated), - intArrayOf(-android.R.attr.state_activated)) + intArrayOf(android.R.attr.state_activated), + intArrayOf(-android.R.attr.state_activated) + ) recipientId - .distinctUntilChanged() - .map { recipientId -> Optional(conversationRepo.getRecipient(recipientId)) } - .switchMap { recipient -> colors.themeObservable(recipient.value) } - .map { theme -> - val textSecondary = context.resolveThemeColor(android.R.attr.textColorSecondary) - ColorStateList(states, intArrayOf(theme.theme, textSecondary)) - } - .autoDisposable(ViewScopeProvider.from(this)) - .subscribe { colorStateList -> - childCount.forEach { index -> - (getChildAt(index) as? TextView)?.setTextColor(colorStateList) - } + .distinctUntilChanged() + .map { recipientId -> Optional(conversationRepo.getRecipient(recipientId)) } + .switchMap { recipient -> colors.themeObservable(recipient.value) } + .map { theme -> + val textSecondary = context.resolveThemeColor(R.attr.textColorSecondary) + ColorStateList(states, intArrayOf(theme.theme, textSecondary)) + } + .autoDispose(ViewScopeProvider.from(this)) + .subscribe { colorStateList -> + childCount.forEach { index -> + (getChildAt(index) as? TextView)?.setTextColor(colorStateList) } + } } } diff --git a/presentation/src/main/java/com/moez/QKSMS/common/widget/PreferenceView.kt b/presentation/src/main/java/com/moez/QKSMS/common/widget/PreferenceView.kt index c14760bba..09d42d2c9 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/widget/PreferenceView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/widget/PreferenceView.kt @@ -77,7 +77,8 @@ class PreferenceView @JvmOverloads constructor( orientation = HORIZONTAL gravity = Gravity.CENTER_VERTICAL - layout.icon.imageTintList = context.resolveThemeColorStateList(android.R.attr.textColorSecondary) + layout.icon.imageTintList = + context.resolveThemeColorStateList(android.R.attr.textColorSecondary) context.obtainStyledAttributes(attrs, R.styleable.PreferenceView).run { title = getString(R.styleable.PreferenceView_title) diff --git a/presentation/src/main/java/com/moez/QKSMS/common/widget/QkContextMenuRecyclerView.kt b/presentation/src/main/java/com/moez/QKSMS/common/widget/QkContextMenuRecyclerView.kt index 8cfd1eb17..cc691bfe4 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/widget/QkContextMenuRecyclerView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/widget/QkContextMenuRecyclerView.kt @@ -11,21 +11,24 @@ import org.prauga.messages.model.MmsPart open class QkContextMenuRecyclerView : RecyclerView { class ViewHolder(view: View) : QkViewHolder(view) { - init { itemView.isLongClickable = true } + init { + itemView.isLongClickable = true + } + var contextMenuValue: VIEW_HOLDER_VALUE_TYPE? = null } abstract class Adapter< - ADAPTER_VALUE_TYPE, - T, - VHT : RecyclerView.ViewHolder + ADAPTER_VALUE_TYPE, + T, + VHT : RecyclerView.ViewHolder > : QkAdapter() { var contextMenuValue: ADAPTER_VALUE_TYPE? = null } private var contextMenuInfo: ContextMenuInfo< - ADAPTER_VALUE_TYPE, - VIEW_HOLDER_VALUE_TYPE + ADAPTER_VALUE_TYPE, + VIEW_HOLDER_VALUE_TYPE >? = null constructor(context: Context) : super(context) diff --git a/presentation/src/main/java/com/moez/QKSMS/common/widget/QkEditText.kt b/presentation/src/main/java/com/moez/QKSMS/common/widget/QkEditText.kt index 0d843064e..821e2097e 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/widget/QkEditText.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/widget/QkEditText.kt @@ -30,11 +30,11 @@ import androidx.core.view.inputmethod.EditorInfoCompat import androidx.core.view.inputmethod.InputConnectionCompat import androidx.core.view.inputmethod.InputContentInfoCompat import com.google.android.mms.ContentType +import io.reactivex.subjects.PublishSubject +import io.reactivex.subjects.Subject import org.prauga.messages.common.util.TextViewStyler import org.prauga.messages.injection.appComponent import org.prauga.messages.util.tryOrNull -import io.reactivex.subjects.PublishSubject -import io.reactivex.subjects.Subject import javax.inject.Inject @@ -44,10 +44,11 @@ import javax.inject.Inject * Beware of updating to extend AppCompatTextView, as this inexplicably breaks the view in * the contacts chip view */ -class QkEditText @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) - : AppCompatEditText(context, attrs) { +class QkEditText @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : + AppCompatEditText(context, attrs) { - @Inject lateinit var textViewStyler: TextViewStyler + @Inject + lateinit var textViewStyler: TextViewStyler val backspaces: Subject = PublishSubject.create() val inputContentSelected: Subject = PublishSubject.create() @@ -64,45 +65,51 @@ class QkEditText @JvmOverloads constructor(context: Context, attrs: AttributeSet override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection { - val inputConnection = object : InputConnectionWrapper(super.onCreateInputConnection(editorInfo), true) { - override fun sendKeyEvent(event: KeyEvent): Boolean { - if (event.action == KeyEvent.ACTION_DOWN && event.keyCode == KeyEvent.KEYCODE_DEL) { - backspaces.onNext(Unit) + val inputConnection = + object : InputConnectionWrapper(super.onCreateInputConnection(editorInfo), true) { + override fun sendKeyEvent(event: KeyEvent): Boolean { + if (event.action == KeyEvent.ACTION_DOWN && event.keyCode == KeyEvent.KEYCODE_DEL) { + backspaces.onNext(Unit) + } + return super.sendKeyEvent(event) } - return super.sendKeyEvent(event) - } - override fun deleteSurroundingText(beforeLength: Int, afterLength: Int): Boolean { - if (beforeLength == 1 && afterLength == 0) { - backspaces.onNext(Unit) + override fun deleteSurroundingText(beforeLength: Int, afterLength: Int): Boolean { + if (beforeLength == 1 && afterLength == 0) { + backspaces.onNext(Unit) + } + return super.deleteSurroundingText(beforeLength, afterLength) } - return super.deleteSurroundingText(beforeLength, afterLength) } - } if (supportsInputContent) { - EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf( + EditorInfoCompat.setContentMimeTypes( + editorInfo, arrayOf( ContentType.IMAGE_JPEG, ContentType.IMAGE_JPG, ContentType.IMAGE_PNG, - ContentType.IMAGE_GIF)) + ContentType.IMAGE_GIF + ) + ) } - val callback = InputConnectionCompat.OnCommitContentListener { inputContentInfo, flags, opts -> - val grantReadPermission = flags and InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION != 0 + val callback = + InputConnectionCompat.OnCommitContentListener { inputContentInfo, flags, opts -> + val grantReadPermission = + flags and InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION != 0 - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1 && grantReadPermission) { - return@OnCommitContentListener tryOrNull { - inputContentInfo.requestPermission() - inputContentSelected.onNext(inputContentInfo) - true - } ?: false + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1 && grantReadPermission) { + return@OnCommitContentListener tryOrNull { + inputContentInfo.requestPermission() + inputContentSelected.onNext(inputContentInfo) + true + } ?: false - } + } - true - } + true + } return InputConnectionCompat.createWrapper(inputConnection, editorInfo, callback) } diff --git a/presentation/src/main/java/com/moez/QKSMS/common/widget/QkSwitch.kt b/presentation/src/main/java/com/moez/QKSMS/common/widget/QkSwitch.kt index 8f2d9068a..123fb703e 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/widget/QkSwitch.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/widget/QkSwitch.kt @@ -30,10 +30,13 @@ import org.prauga.messages.injection.appComponent import org.prauga.messages.util.Preferences import javax.inject.Inject -class QkSwitch @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : SwitchCompat(context, attrs) { +class QkSwitch @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : + SwitchCompat(context, attrs) { - @Inject lateinit var colors: Colors - @Inject lateinit var prefs: Preferences + @Inject + lateinit var colors: Colors + @Inject + lateinit var prefs: Preferences init { if (!isInEditMode) { @@ -46,19 +49,26 @@ class QkSwitch @JvmOverloads constructor(context: Context, attrs: AttributeSet? if (!isInEditMode) { val states = arrayOf( - intArrayOf(-android.R.attr.state_enabled), - intArrayOf(android.R.attr.state_checked), - intArrayOf()) + intArrayOf(-android.R.attr.state_enabled), + intArrayOf(android.R.attr.state_checked), + intArrayOf() + ) - thumbTintList = ColorStateList(states, intArrayOf( + thumbTintList = ColorStateList( + states, intArrayOf( context.resolveThemeColor(R.attr.switchThumbDisabled), colors.theme().theme, - context.resolveThemeColor(R.attr.switchThumbEnabled))) + context.resolveThemeColor(R.attr.switchThumbEnabled) + ) + ) - trackTintList = ColorStateList(states, intArrayOf( + trackTintList = ColorStateList( + states, intArrayOf( context.resolveThemeColor(R.attr.switchTrackDisabled), colors.theme().theme.withAlpha(0x4D), - context.resolveThemeColor(R.attr.switchTrackEnabled))) + context.resolveThemeColor(R.attr.switchTrackEnabled) + ) + ) } } } \ No newline at end of file diff --git a/presentation/src/main/java/com/moez/QKSMS/common/widget/QkTextView.kt b/presentation/src/main/java/com/moez/QKSMS/common/widget/QkTextView.kt index 880db35b9..b2401edce 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/widget/QkTextView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/widget/QkTextView.kt @@ -29,7 +29,8 @@ open class QkTextView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null ) : AppCompatTextView(context, attrs) { - @Inject lateinit var textViewStyler: TextViewStyler + @Inject + lateinit var textViewStyler: TextViewStyler /** * Collapse a multiline list of strings into a single line @@ -59,15 +60,15 @@ open class QkTextView @JvmOverloads constructor( if (collapseEnabled) { layout - ?.takeIf { layout -> layout.lineCount > 0 } - ?.let { layout -> layout.getEllipsisCount(layout.lineCount - 1) } - ?.takeIf { ellipsisCount -> ellipsisCount > 0 } - ?.let { ellipsisCount -> text.dropLast(ellipsisCount).lastIndexOf(',') } - ?.takeIf { lastComma -> lastComma >= 0 } - ?.let { lastComma -> - val remainingNames = text.drop(lastComma).count { c -> c == ',' } - text = "${text.take(lastComma)}, +$remainingNames" - } + ?.takeIf { layout -> layout.lineCount > 0 } + ?.let { layout -> layout.getEllipsisCount(layout.lineCount - 1) } + ?.takeIf { ellipsisCount -> ellipsisCount > 0 } + ?.let { ellipsisCount -> text.dropLast(ellipsisCount).lastIndexOf(',') } + ?.takeIf { lastComma -> lastComma >= 0 } + ?.let { lastComma -> + val remainingNames = text.drop(lastComma).count { c -> c == ',' } + text = "${text.take(lastComma)}, +$remainingNames" + } } } diff --git a/presentation/src/main/java/com/moez/QKSMS/common/widget/RadioPreferenceView.kt b/presentation/src/main/java/com/moez/QKSMS/common/widget/RadioPreferenceView.kt index eef169351..85a559a95 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/widget/RadioPreferenceView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/widget/RadioPreferenceView.kt @@ -39,7 +39,8 @@ class RadioPreferenceView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null ) : ConstraintLayout(context, attrs) { - @Inject lateinit var colors: Colors + @Inject + lateinit var colors: Colors private var layout: RadioPreferenceViewBinding val radioButton get() = layout.radioButton @@ -82,15 +83,17 @@ class RadioPreferenceView @JvmOverloads constructor( setBackgroundResource(context.resolveThemeAttribute(R.attr.selectableItemBackground)) val states = arrayOf( - intArrayOf(android.R.attr.state_checked), - intArrayOf(-android.R.attr.state_checked)) + intArrayOf(android.R.attr.state_checked), + intArrayOf(-android.R.attr.state_checked) + ) val themeColor = when (isInEditMode) { true -> context.resources.getColor(R.color.tools_theme) false -> colors.theme().theme } val textSecondary = context.resolveThemeColor(android.R.attr.textColorTertiary) - layout.radioButton.buttonTintList = ColorStateList(states, intArrayOf(themeColor, textSecondary)) + layout.radioButton.buttonTintList = + ColorStateList(states, intArrayOf(themeColor, textSecondary)) layout.radioButton.forwardTouches(this) context.obtainStyledAttributes(attrs, R.styleable.RadioPreferenceView)?.run { @@ -98,9 +101,10 @@ class RadioPreferenceView @JvmOverloads constructor( summary = getString(R.styleable.RadioPreferenceView_summary) // If there's a custom view used for the preference's widget, inflate it - getResourceId(R.styleable.RadioPreferenceView_widget, -1).takeIf { it != -1 }?.let { id -> - View.inflate(context, id, layout.widgetFrame) - } + getResourceId(R.styleable.RadioPreferenceView_widget, -1).takeIf { it != -1 } + ?.let { id -> + View.inflate(context, id, layout.widgetFrame) + } recycle() } diff --git a/presentation/src/main/java/com/moez/QKSMS/common/widget/SquareImageView.kt b/presentation/src/main/java/com/moez/QKSMS/common/widget/SquareImageView.kt index 56acd6896..dd4c7d2e4 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/widget/SquareImageView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/widget/SquareImageView.kt @@ -22,7 +22,8 @@ import android.content.Context import android.util.AttributeSet import android.widget.ImageView -class SquareImageView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : ImageView(context, attrs) { +class SquareImageView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : + ImageView(context, attrs) { override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec, widthMeasureSpec) diff --git a/presentation/src/main/java/com/moez/QKSMS/common/widget/TightTextView.kt b/presentation/src/main/java/com/moez/QKSMS/common/widget/TightTextView.kt index 072fc7663..18eebb913 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/widget/TightTextView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/widget/TightTextView.kt @@ -36,12 +36,14 @@ class TightTextView @JvmOverloads constructor( } val maxLineWidth = (0 until layout.lineCount) - .map(layout::getLineWidth) - .max() ?: 0f + .map(layout::getLineWidth) + .max() ?: 0f - val width = Math.ceil(maxLineWidth.toDouble()).toInt() + compoundPaddingLeft + compoundPaddingRight + val width = + Math.ceil(maxLineWidth.toDouble()).toInt() + compoundPaddingLeft + compoundPaddingRight if (width < measuredWidth) { - val widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.getMode(widthMeasureSpec)) + val widthSpec = + MeasureSpec.makeMeasureSpec(width, MeasureSpec.getMode(widthMeasureSpec)) super.onMeasure(widthSpec, heightMeasureSpec) } } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/backup/BackupActivity.kt b/presentation/src/main/java/com/moez/QKSMS/feature/backup/BackupActivity.kt index 4b140fd7a..383803b5c 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/backup/BackupActivity.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/backup/BackupActivity.kt @@ -23,12 +23,12 @@ import com.bluelinelabs.conductor.Conductor import com.bluelinelabs.conductor.Router import com.bluelinelabs.conductor.RouterTransaction import dagger.android.AndroidInjection -import org.prauga.messages.R import org.prauga.messages.common.base.QkThemedActivity import org.prauga.messages.databinding.ContainerActivityBinding -class BackupActivity : QkThemedActivity(ContainerActivityBinding::inflate) { +class BackupActivity : + QkThemedActivity(ContainerActivityBinding::inflate) { private lateinit var router: Router diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/backup/BackupController.kt b/presentation/src/main/java/com/moez/QKSMS/feature/backup/BackupController.kt index 03cb72551..fa1c9fa50 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/backup/BackupController.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/backup/BackupController.kt @@ -23,12 +23,19 @@ import android.content.res.ColorStateList import android.graphics.Typeface import android.net.Uri import android.view.View +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.ProgressBar +import android.widget.TextView import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AlertDialog import androidx.core.view.children import androidx.core.view.isVisible import com.jakewharton.rxbinding2.view.clicks +import io.reactivex.Observable +import io.reactivex.subjects.PublishSubject +import io.reactivex.subjects.Subject import org.prauga.messages.R import org.prauga.messages.common.base.QkController import org.prauga.messages.common.util.QkActivityResultContracts @@ -41,18 +48,12 @@ import org.prauga.messages.common.util.extensions.setTint import org.prauga.messages.common.widget.PreferenceView import org.prauga.messages.injection.appComponent import org.prauga.messages.repository.BackupRepository -import io.reactivex.Observable -import io.reactivex.subjects.PublishSubject -import io.reactivex.subjects.Subject -import android.widget.ImageView -import android.widget.LinearLayout -import android.widget.ProgressBar -import android.widget.TextView import javax.inject.Inject class BackupController : QkController(), BackupView { - @Inject override lateinit var presenter: BackupPresenter + @Inject + override lateinit var presenter: BackupPresenter private lateinit var linearLayout: LinearLayout private lateinit var location: PreferenceView @@ -83,40 +84,40 @@ class BackupController : QkController( private val stopRestoreDialog by lazy { AlertDialog.Builder(activity!!, R.style.AppThemeDialog) - .setTitle(R.string.backup_restore_stop_title) - .setMessage(R.string.backup_restore_stop_message) - .setPositiveButton(R.string.button_stop, stopRestoreConfirmSubject) - .setNegativeButton(R.string.button_cancel, stopRestoreCancelSubject) - .setCancelable(false) - .create() + .setTitle(R.string.backup_restore_stop_title) + .setMessage(R.string.backup_restore_stop_message) + .setPositiveButton(R.string.button_stop, stopRestoreConfirmSubject) + .setNegativeButton(R.string.button_cancel, stopRestoreCancelSubject) + .setCancelable(false) + .create() } private val selectLocationRationaleDialog by lazy { AlertDialog.Builder(activity!!, R.style.AppThemeDialog) - .setTitle(R.string.backup_select_location_rationale_title) - .setMessage(R.string.backup_select_location_rationale_message) - .setPositiveButton(R.string.button_continue, selectFolderConfirmSubject) - .setNegativeButton(R.string.button_cancel, selectFolderCancelSubject) - .setCancelable(false) - .create() + .setTitle(R.string.backup_select_location_rationale_title) + .setMessage(R.string.backup_select_location_rationale_message) + .setPositiveButton(R.string.button_continue, selectFolderConfirmSubject) + .setNegativeButton(R.string.button_cancel, selectFolderCancelSubject) + .setCancelable(false) + .create() } private val selectedBackupErrorDialog by lazy { AlertDialog.Builder(activity!!, R.style.AppThemeDialog) - .setTitle(R.string.backup_selected_backup_error_title) - .setMessage(R.string.backup_selected_backup_error_message) - .setPositiveButton(R.string.button_continue, restoreErrorConfirmSubject) - .setCancelable(false) - .create() + .setTitle(R.string.backup_selected_backup_error_title) + .setMessage(R.string.backup_selected_backup_error_message) + .setPositiveButton(R.string.button_continue, restoreErrorConfirmSubject) + .setCancelable(false) + .create() } private val selectedBackupDetailsDialog by lazy { AlertDialog.Builder(activity!!, R.style.AppThemeDialog) - .setTitle(R.string.backup_selected_backup_details_title) - .setPositiveButton(R.string.backup_restore_title, confirmRestoreConfirmSubject) - .setNegativeButton(R.string.button_cancel, confirmRestoreCancelSubject) - .setCancelable(false) - .create() + .setTitle(R.string.backup_selected_backup_details_title) + .setPositiveButton(R.string.backup_restore_title, confirmRestoreConfirmSubject) + .setNegativeButton(R.string.button_cancel, confirmRestoreCancelSubject) + .setCancelable(false) + .create() } private lateinit var openDirectory: ActivityResultLauncher @@ -175,9 +176,9 @@ class BackupController : QkController( // Make the list titles bold linearLayout.children - .mapNotNull { it as? PreferenceView } - .map { it.findViewById(R.id.titleView) } - .forEach { it.setTypeface(it.typeface, Typeface.BOLD) } + .mapNotNull { it as? PreferenceView } + .map { it.findViewById(R.id.titleView) } + .forEach { it.setTypeface(it.typeface, Typeface.BOLD) } } override fun render(state: BackupState) { @@ -227,15 +228,19 @@ class BackupController : QkController( stopRestoreDialog.setShowing(state.showStopRestoreDialog) - fabIcon.setImageResource(when (state.upgraded) { - true -> R.drawable.ic_file_upload_black_24dp - false -> R.drawable.ic_star_black_24dp - }) + fabIcon.setImageResource( + when (state.upgraded) { + true -> R.drawable.ic_file_upload_black_24dp + false -> R.drawable.ic_star_black_24dp + } + ) - fabLabel.setText(when (state.upgraded) { - true -> R.string.backup_now - false -> R.string.title_qksms_plus - }) + fabLabel.setText( + when (state.upgraded) { + true -> R.string.backup_now + false -> R.string.title_qksms_plus + } + ) } override fun setBackupLocationClicks(): Observable<*> = location.clicks() @@ -269,9 +274,12 @@ class BackupController : QkController( } override fun selectFile(initialUri: Uri) { - openDocument.launch(QkActivityResultContracts.OpenDocumentParams( + openDocument.launch( + QkActivityResultContracts.OpenDocumentParams( mimeTypes = listOf("application/json", "application/octet-stream"), - initialUri = initialUri)) + initialUri = initialUri + ) + ) } } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/backup/BackupPresenter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/backup/BackupPresenter.kt index e0be6ad31..125e808f8 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/backup/BackupPresenter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/backup/BackupPresenter.kt @@ -20,7 +20,10 @@ package org.prauga.messages.feature.backup import android.content.Context import com.uber.autodispose.android.lifecycle.scope -import com.uber.autodispose.autoDisposable +import com.uber.autodispose.autoDispose +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.rxkotlin.plusAssign +import io.reactivex.schedulers.Schedulers import org.prauga.messages.R import org.prauga.messages.common.Navigator import org.prauga.messages.common.base.QkPresenter @@ -29,9 +32,6 @@ import org.prauga.messages.common.util.extensions.makeToast import org.prauga.messages.interactor.PerformBackup import org.prauga.messages.manager.BillingManager import org.prauga.messages.repository.BackupRepository -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.rxkotlin.plusAssign -import io.reactivex.schedulers.Schedulers import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -46,111 +46,114 @@ class BackupPresenter @Inject constructor( init { disposables += backupRepo.getBackupProgress() - .sample(16, TimeUnit.MILLISECONDS) - .distinctUntilChanged() - .subscribe { progress -> newState { copy(backupProgress = progress) } } + .sample(16, TimeUnit.MILLISECONDS) + .distinctUntilChanged() + .subscribe { progress -> newState { copy(backupProgress = progress) } } disposables += backupRepo.getRestoreProgress() - .sample(16, TimeUnit.MILLISECONDS) - .distinctUntilChanged() - .subscribe { progress -> newState { copy(restoreProgress = progress) } } + .sample(16, TimeUnit.MILLISECONDS) + .distinctUntilChanged() + .subscribe { progress -> newState { copy(restoreProgress = progress) } } disposables += billingManager.upgradeStatus - .subscribe { upgraded -> newState { copy(upgraded = upgraded) } } + .subscribe { upgraded -> newState { copy(upgraded = upgraded) } } } override fun bindIntents(view: BackupView) { super.bindIntents(view) view.setBackupLocationClicks() - .observeOn(AndroidSchedulers.mainThread()) - .autoDisposable(view.scope()) - .subscribe { view.selectFolder(backupRepo.getBackupPathUriForPicker()) } + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(view.scope()) + .subscribe { view.selectFolder(backupRepo.getBackupPathUriForPicker()) } view.restoreClicks() - .withLatestFrom( - backupRepo.getBackupProgress(), - backupRepo.getRestoreProgress(), - billingManager.upgradeStatus) - { _, backupProgress, restoreProgress, upgraded -> - when { - !upgraded -> context.makeToast(R.string.backup_restore_error_plus) - backupProgress.running -> context.makeToast(R.string.backup_restore_error_backup) - restoreProgress.running -> context.makeToast(R.string.backup_restore_error_restore) - else -> view.selectFile(backupRepo.getBackupPathUriForPicker()) - } + .withLatestFrom( + backupRepo.getBackupProgress(), + backupRepo.getRestoreProgress(), + billingManager.upgradeStatus + ) + { _, backupProgress, restoreProgress, upgraded -> + when { + !upgraded -> context.makeToast(R.string.backup_restore_error_plus) + backupProgress.running -> context.makeToast(R.string.backup_restore_error_backup) + restoreProgress.running -> context.makeToast(R.string.backup_restore_error_restore) + else -> view.selectFile(backupRepo.getBackupPathUriForPicker()) } - .autoDisposable(view.scope()) - .subscribe() + } + .autoDispose(view.scope()) + .subscribe() view.backupClicks() - .withLatestFrom(billingManager.upgradeStatus) { _, upgraded -> upgraded } - .autoDisposable(view.scope()) - .subscribe { upgraded -> - when { - backupRepo.getBackupDocumentTree() == null -> { - newState { copy(showLocationRationale = true) } - } - !upgraded -> navigator.showQksmsPlusActivity("backup_fab") - upgraded -> performBackup.execute(Unit) + .withLatestFrom(billingManager.upgradeStatus) { _, upgraded -> upgraded } + .autoDispose(view.scope()) + .subscribe { upgraded -> + when { + backupRepo.getBackupDocumentTree() == null -> { + newState { copy(showLocationRationale = true) } } + + !upgraded -> navigator.showQksmsPlusActivity("backup_fab") + upgraded -> performBackup.execute(Unit) } + } view.locationRationaleConfirmClicks() - .doOnNext { newState { copy(showLocationRationale = false) } } - .autoDisposable(view.scope()) - .subscribe { view.selectFolder(backupRepo.getBackupPathUriForPicker()) } + .doOnNext { newState { copy(showLocationRationale = false) } } + .autoDispose(view.scope()) + .subscribe { view.selectFolder(backupRepo.getBackupPathUriForPicker()) } view.locationRationaleCancelClicks() - .doOnNext { newState { copy(showLocationRationale = false) } } - .autoDisposable(view.scope()) - .subscribe() + .doOnNext { newState { copy(showLocationRationale = false) } } + .autoDispose(view.scope()) + .subscribe() view.selectedBackupErrorClicks() - .autoDisposable(view.scope()) - .subscribe { newState { copy(showSelectedBackupError = false) } } + .autoDispose(view.scope()) + .subscribe { newState { copy(showSelectedBackupError = false) } } view.confirmRestoreBackupConfirmClicks() - .doOnNext { newState { copy(selectedBackupDetails = null) } } - .withLatestFrom(view.documentSelected()) { _, backup -> backup } - .autoDisposable(view.scope()) - .subscribe { backup -> RestoreBackupService.start(context, backup) } + .doOnNext { newState { copy(selectedBackupDetails = null) } } + .withLatestFrom(view.documentSelected()) { _, backup -> backup } + .autoDispose(view.scope()) + .subscribe { backup -> RestoreBackupService.start(context, backup) } view.confirmRestoreBackupCancelClicks() - .doOnNext { newState { copy(selectedBackupDetails = null) } } - .autoDisposable(view.scope()) - .subscribe() + .doOnNext { newState { copy(selectedBackupDetails = null) } } + .autoDispose(view.scope()) + .subscribe() view.stopRestoreClicks() - .autoDisposable(view.scope()) - .subscribe { newState { copy(showStopRestoreDialog = true) } } + .autoDispose(view.scope()) + .subscribe { newState { copy(showStopRestoreDialog = true) } } view.stopRestoreConfirmed() - .doOnNext { newState { copy(showStopRestoreDialog = false) } } - .autoDisposable(view.scope()) - .subscribe { backupRepo.stopRestore() } + .doOnNext { newState { copy(showStopRestoreDialog = false) } } + .autoDispose(view.scope()) + .subscribe { backupRepo.stopRestore() } view.stopRestoreCancel() - .autoDisposable(view.scope()) - .subscribe { newState { copy(showStopRestoreDialog = false) } } + .autoDispose(view.scope()) + .subscribe { newState { copy(showStopRestoreDialog = false) } } view.documentTreeSelected() - .autoDisposable(view.scope()) - .subscribe { uri -> backupRepo.persistBackupDirectory(uri) } + .autoDispose(view.scope()) + .subscribe { uri -> backupRepo.persistBackupDirectory(uri) } view.documentSelected() - .observeOn(Schedulers.io()) - .autoDisposable(view.scope()) - .subscribe { uri -> - try { - val backupFile = backupRepo.parseBackup(uri) - val date = dateFormatter.getDetailedTimestamp(backupFile.date) - val details = context.getString(R.string.backup_details, date, backupFile.messages) - newState { copy(selectedBackupDetails = details) } - } catch (e: Exception) { - newState { copy(showSelectedBackupError = true) } - } + .observeOn(Schedulers.io()) + .autoDispose(view.scope()) + .subscribe { uri -> + try { + val backupFile = backupRepo.parseBackup(uri) + val date = dateFormatter.getDetailedTimestamp(backupFile.date) + val details = + context.getString(R.string.backup_details, date, backupFile.messages) + newState { copy(selectedBackupDetails = details) } + } catch (e: Exception) { + newState { copy(showSelectedBackupError = true) } } + } } } \ No newline at end of file diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/backup/BackupView.kt b/presentation/src/main/java/com/moez/QKSMS/feature/backup/BackupView.kt index 3db0404d4..59f5cc64d 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/backup/BackupView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/backup/BackupView.kt @@ -19,8 +19,8 @@ package org.prauga.messages.feature.backup import android.net.Uri -import org.prauga.messages.common.base.QkViewContract import io.reactivex.Observable +import org.prauga.messages.common.base.QkViewContract interface BackupView : QkViewContract { fun setBackupLocationClicks(): Observable<*> diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/backup/RestoreBackupService.kt b/presentation/src/main/java/com/moez/QKSMS/feature/backup/RestoreBackupService.kt index e5b28e773..153aab37a 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/backup/RestoreBackupService.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/backup/RestoreBackupService.kt @@ -27,11 +27,11 @@ import android.os.IBinder import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat import dagger.android.AndroidInjection +import io.reactivex.Observable +import io.reactivex.schedulers.Schedulers import org.prauga.messages.common.util.extensions.getLabel import org.prauga.messages.manager.NotificationManager import org.prauga.messages.repository.BackupRepository -import io.reactivex.Observable -import io.reactivex.schedulers.Schedulers import timber.log.Timber import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -48,15 +48,17 @@ class RestoreBackupService : Service() { fun start(context: Context, backupFile: Uri) { val intent = Intent(context, RestoreBackupService::class.java) - .setAction(ACTION_START) - .putExtra(EXTRA_FILE_URI, backupFile.toString()) + .setAction(ACTION_START) + .putExtra(EXTRA_FILE_URI, backupFile.toString()) ContextCompat.startForegroundService(context, intent) } } - @Inject lateinit var backupRepo: BackupRepository - @Inject lateinit var notificationManager: NotificationManager + @Inject + lateinit var backupRepo: BackupRepository + @Inject + lateinit var notificationManager: NotificationManager private val notification by lazy { notificationManager.getNotificationForBackup() } @@ -81,30 +83,30 @@ class RestoreBackupService : Service() { startForeground(NOTIFICATION_ID, notification.build()) backupRepo.getRestoreProgress() - .sample(200, TimeUnit.MILLISECONDS, true) - .subscribeOn(Schedulers.io()) - .subscribe { progress -> - when (progress) { - is BackupRepository.Progress.Idle -> stop() - - is BackupRepository.Progress.Running -> notification - .setProgress(progress.max, progress.count, progress.indeterminate) - .setContentText(progress.getLabel(this)) - .let { notificationManager.notify(NOTIFICATION_ID, it.build()) } - - else -> notification - .setProgress(0, 0, progress.indeterminate) - .setContentText(progress.getLabel(this)) - .let { notificationManager.notify(NOTIFICATION_ID, it.build()) } - } + .sample(200, TimeUnit.MILLISECONDS, true) + .subscribeOn(Schedulers.io()) + .subscribe { progress -> + when (progress) { + is BackupRepository.Progress.Idle -> stop() + + is BackupRepository.Progress.Running -> notification + .setProgress(progress.max, progress.count, progress.indeterminate) + .setContentText(progress.getLabel(this)) + .let { notificationManager.notify(NOTIFICATION_ID, it.build()) } + + else -> notification + .setProgress(0, 0, progress.indeterminate) + .setContentText(progress.getLabel(this)) + .let { notificationManager.notify(NOTIFICATION_ID, it.build()) } } + } // Start the restore Observable.just(intent) - .map { Uri.parse(it.getStringExtra(EXTRA_FILE_URI)) } - .map(backupRepo::performRestore) - .subscribeOn(Schedulers.io()) - .subscribe({}, Timber::w) + .map { Uri.parse(it.getStringExtra(EXTRA_FILE_URI)) } + .map(backupRepo::performRestore) + .subscribeOn(Schedulers.io()) + .subscribe({}, Timber::w) } private fun stop() { diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/blocking/BlockingPresenter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/blocking/BlockingPresenter.kt index 4c1fc1dff..a521fb520 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/blocking/BlockingPresenter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/blocking/BlockingPresenter.kt @@ -21,6 +21,7 @@ package org.prauga.messages.feature.blocking import android.content.Context import com.uber.autodispose.android.lifecycle.scope import com.uber.autodispose.autoDisposable +import com.uber.autodispose.autoDispose import org.prauga.messages.R import org.prauga.messages.blocking.BlockingClient import org.prauga.messages.common.base.QkPresenter @@ -55,31 +56,31 @@ class BlockingPresenter @Inject constructor( super.bindIntents(view) view.blockingManagerIntent - .autoDisposable(view.scope()) - .subscribe { view.openBlockingManager() } + .autoDispose(view.scope()) + .subscribe { view.openBlockingManager() } view.blockedNumbersIntent - .autoDisposable(view.scope()) - .subscribe { - if (prefs.blockingManager.get() == Preferences.BLOCKING_MANAGER_QKSMS) { - // TODO: This is a hack, get rid of it once we implement AndroidX navigation - view.openBlockedNumbers() - } else { - blockingClient.openSettings() - } + .autoDispose(view.scope()) + .subscribe { + if (prefs.blockingManager.get() == Preferences.BLOCKING_MANAGER_QKSMS) { + // TODO: This is a hack, get rid of it once we implement AndroidX navigation + view.openBlockedNumbers() + } else { + blockingClient.openSettings() } + } view.messageContentFiltersIntent - .autoDisposable(view.scope()) - .subscribe { view.openMessageContentFilters() } + .autoDispose(view.scope()) + .subscribe { view.openMessageContentFilters() } view.blockedMessagesIntent - .autoDisposable(view.scope()) - .subscribe { view.openBlockedMessages() } + .autoDispose(view.scope()) + .subscribe { view.openBlockedMessages() } view.dropClickedIntent - .autoDisposable(view.scope()) - .subscribe { prefs.drop.set(!prefs.drop.get()) } + .autoDispose(view.scope()) + .subscribe { prefs.drop.set(!prefs.drop.get()) } } } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/blocking/filters/MessageContentFiltersAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/blocking/filters/MessageContentFiltersAdapter.kt index a21d4faa3..390d1a9e2 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/blocking/filters/MessageContentFiltersAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/blocking/filters/MessageContentFiltersAdapter.kt @@ -21,20 +21,27 @@ package org.prauga.messages.feature.blocking.filters import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import org.prauga.messages.R -import org.prauga.messages.common.base.QkRealmAdapter -import org.prauga.messages.common.base.QkBindingViewHolder -import org.prauga.messages.model.MessageContentFilter import io.reactivex.subjects.PublishSubject import io.reactivex.subjects.Subject +import org.prauga.messages.common.base.QkBindingViewHolder +import org.prauga.messages.common.base.QkRealmAdapter import org.prauga.messages.databinding.MessageContentFilterListItemBinding +import org.prauga.messages.model.MessageContentFilter -class MessageContentFiltersAdapter : QkRealmAdapter>() { +class MessageContentFiltersAdapter : + QkRealmAdapter>() { val removeMessageContentFilter: Subject = PublishSubject.create() - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QkBindingViewHolder { - val binding = MessageContentFilterListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): QkBindingViewHolder { + val binding = MessageContentFilterListItemBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) return QkBindingViewHolder(binding).apply { binding.removeFilter.setOnClickListener { val filter = getItem(adapterPosition) ?: return@setOnClickListener @@ -43,11 +50,15 @@ class MessageContentFiltersAdapter : QkRealmAdapter, position: Int) { + override fun onBindViewHolder( + holder: QkBindingViewHolder, + position: Int + ) { val item = getItem(position)!! holder.binding.caseIcon.visibility = if (item.caseSensitive) View.VISIBLE else View.GONE holder.binding.regexIcon.visibility = if (item.isRegex) View.VISIBLE else View.GONE - holder.binding.contactsIcon.visibility = if (item.includeContacts) View.VISIBLE else View.GONE + holder.binding.contactsIcon.visibility = + if (item.includeContacts) View.VISIBLE else View.GONE holder.binding.filter.text = item.value } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/blocking/filters/MessageContentFiltersController.kt b/presentation/src/main/java/com/moez/QKSMS/feature/blocking/filters/MessageContentFiltersController.kt index 052d9ab7e..2f357a672 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/blocking/filters/MessageContentFiltersController.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/blocking/filters/MessageContentFiltersController.kt @@ -20,10 +20,18 @@ package org.prauga.messages.feature.blocking.filters import android.view.LayoutInflater import android.view.View +import android.widget.CompoundButton +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView import androidx.appcompat.app.AlertDialog +import androidx.recyclerview.widget.RecyclerView import com.jakewharton.rxbinding2.view.clicks import com.uber.autodispose.android.lifecycle.scope -import com.uber.autodispose.autoDisposable +import com.uber.autodispose.autoDispose +import io.reactivex.Observable +import io.reactivex.subjects.PublishSubject +import io.reactivex.subjects.Subject import org.prauga.messages.R import org.prauga.messages.common.base.QkController import org.prauga.messages.common.util.Colors @@ -32,21 +40,16 @@ import org.prauga.messages.common.util.extensions.setTint import org.prauga.messages.common.widget.PreferenceView import org.prauga.messages.injection.appComponent import org.prauga.messages.model.MessageContentFilterData -import io.reactivex.Observable -import io.reactivex.subjects.PublishSubject -import io.reactivex.subjects.Subject -import android.widget.CompoundButton -import android.widget.ImageView -import android.widget.LinearLayout -import android.widget.TextView -import androidx.recyclerview.widget.RecyclerView import javax.inject.Inject -class MessageContentFiltersController : QkController(), MessageContentFiltersView { +class MessageContentFiltersController : + QkController(), MessageContentFiltersView { - @Inject override lateinit var presenter: MessageContentFiltersPresenter - @Inject lateinit var colors: Colors + @Inject + override lateinit var presenter: MessageContentFiltersPresenter + @Inject + lateinit var colors: Colors private lateinit var add: ImageView private lateinit var filters: RecyclerView @@ -92,7 +95,8 @@ class MessageContentFiltersController : QkController = saveFilterSubject override fun showAddDialog() { - val layout = LayoutInflater.from(activity).inflate(R.layout.message_content_filters_add_dialog, null) + val layout = + LayoutInflater.from(activity).inflate(R.layout.message_content_filters_add_dialog, null) val addDialog = layout.findViewById(R.id.add_dialog) val input = layout.findViewById(R.id.input) val caseSensitivity = layout.findViewById(R.id.caseSensitivity) @@ -104,32 +108,36 @@ class MessageContentFiltersController : QkController view as? PreferenceView } .map { preference -> preference.clicks().map { preference } } .let { Observable.merge(it) } - .autoDisposable(scope()) + .autoDispose(scope()) .subscribe { it.findViewById(R.id.checkbox)?.let { checkbox -> checkbox.isChecked = !checkbox.isChecked } - caseSensitivity.isEnabled = !(regexp.findViewById(R.id.checkbox)?.isChecked ?: false) + caseSensitivity.isEnabled = + !(regexp.findViewById(R.id.checkbox)?.isChecked ?: false) } val dialog = AlertDialog.Builder(activity!!, R.style.AppThemeDialog) - .setView(layout) - .setPositiveButton(R.string.message_content_filters_dialog_create) { _, _ -> - var text = input.text.toString(); - if (!text.isBlank()) { - if (!(regexp.findViewById(R.id.checkbox)?.isChecked ?: false)) text = text.trim() - saveFilterSubject.onNext( - MessageContentFilterData( - text, - (caseSensitivity.findViewById(R.id.checkbox)?.isChecked == true) && - !(regexp.findViewById(R.id.checkbox)?.isChecked ?: false), - regexp.findViewById(R.id.checkbox)?.isChecked == true, - contacts.findViewById(R.id.checkbox)?.isChecked == true - ) + .setView(layout) + .setPositiveButton(R.string.message_content_filters_dialog_create) { _, _ -> + var text = input.text.toString(); + if (!text.isBlank()) { + if (!(regexp.findViewById(R.id.checkbox)?.isChecked + ?: false) + ) text = text.trim() + saveFilterSubject.onNext( + MessageContentFilterData( + text, + (caseSensitivity.findViewById(R.id.checkbox)?.isChecked == true) && + !(regexp.findViewById(R.id.checkbox)?.isChecked + ?: false), + regexp.findViewById(R.id.checkbox)?.isChecked == true, + contacts.findViewById(R.id.checkbox)?.isChecked == true ) - } + ) } - .setNegativeButton(R.string.button_cancel) { _, _ -> } + } + .setNegativeButton(R.string.button_cancel) { _, _ -> } dialog.show() } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/blocking/filters/MessageContentFiltersPresenter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/blocking/filters/MessageContentFiltersPresenter.kt index 0c45eb181..fe70b3297 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/blocking/filters/MessageContentFiltersPresenter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/blocking/filters/MessageContentFiltersPresenter.kt @@ -19,16 +19,16 @@ package org.prauga.messages.feature.blocking.filters import com.uber.autodispose.android.lifecycle.scope -import com.uber.autodispose.autoDisposable +import com.uber.autodispose.autoDispose +import io.reactivex.schedulers.Schedulers import org.prauga.messages.common.base.QkPresenter import org.prauga.messages.repository.MessageContentFilterRepository -import io.reactivex.schedulers.Schedulers import javax.inject.Inject class MessageContentFiltersPresenter @Inject constructor( private val filterRepo: MessageContentFilterRepository, ) : QkPresenter( - MessageContentFiltersState(filters = filterRepo.getMessageContentFilters()) + MessageContentFiltersState(filters = filterRepo.getMessageContentFilters()) ) { override fun bindIntents(view: MessageContentFiltersView) { @@ -38,17 +38,17 @@ class MessageContentFiltersPresenter @Inject constructor( .observeOn(Schedulers.io()) .doOnNext(filterRepo::removeFilter) .subscribeOn(Schedulers.io()) - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe() view.addFilter() - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe { view.showAddDialog() } view.saveFilter() .observeOn(Schedulers.io()) .subscribeOn(Schedulers.io()) - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe { filterData -> filterRepo.createFilter(filterData) } } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/blocking/filters/MessageContentFiltersState.kt b/presentation/src/main/java/com/moez/QKSMS/feature/blocking/filters/MessageContentFiltersState.kt index f0e4b834a..66f3e6298 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/blocking/filters/MessageContentFiltersState.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/blocking/filters/MessageContentFiltersState.kt @@ -18,8 +18,8 @@ */ package org.prauga.messages.feature.blocking.filters -import org.prauga.messages.model.MessageContentFilter import io.realm.RealmResults +import org.prauga.messages.model.MessageContentFilter data class MessageContentFiltersState( val filters: RealmResults? = null diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/blocking/filters/MessageContentFiltersView.kt b/presentation/src/main/java/com/moez/QKSMS/feature/blocking/filters/MessageContentFiltersView.kt index 01d4b3799..d55ab7aa3 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/blocking/filters/MessageContentFiltersView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/blocking/filters/MessageContentFiltersView.kt @@ -18,9 +18,9 @@ */ package org.prauga.messages.feature.blocking.filters +import io.reactivex.Observable import org.prauga.messages.common.base.QkViewContract import org.prauga.messages.model.MessageContentFilterData -import io.reactivex.Observable interface MessageContentFiltersView : QkViewContract { diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/blocking/manager/BlockingManagerPresenter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/blocking/manager/BlockingManagerPresenter.kt index 470dcfbe5..c8e27719b 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/blocking/manager/BlockingManagerPresenter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/blocking/manager/BlockingManagerPresenter.kt @@ -3,6 +3,7 @@ package org.prauga.messages.feature.blocking.manager import android.content.Context import com.uber.autodispose.android.lifecycle.scope import com.uber.autodispose.autoDisposable +import com.uber.autodispose.autoDispose import org.prauga.messages.R import org.prauga.messages.blocking.BlockingClient import org.prauga.messages.blocking.CallBlockerBlockingClient @@ -28,112 +29,121 @@ class BlockingManagerPresenter @Inject constructor( private val prefs: Preferences, private val qksms: QksmsBlockingClient, private val shouldIAnswer: ShouldIAnswerBlockingClient -) : QkPresenter(BlockingManagerState( +) : QkPresenter( + BlockingManagerState( blockingManager = prefs.blockingManager.get(), callBlockerInstalled = callBlocker.isAvailable(), callControlInstalled = callControl.isAvailable(), siaInstalled = shouldIAnswer.isAvailable() -)) { + ) +) { init { disposables += prefs.blockingManager.asObservable() - .subscribe { manager -> newState { copy(blockingManager = manager) } } + .subscribe { manager -> newState { copy(blockingManager = manager) } } } override fun bindIntents(view: BlockingManagerView) { super.bindIntents(view) view.activityResumed() - .map { callBlocker.isAvailable() } - .distinctUntilChanged() - .autoDisposable(view.scope()) - .subscribe { available -> newState { copy(callBlockerInstalled = available) } } + .map { callBlocker.isAvailable() } + .distinctUntilChanged() + .autoDispose(view.scope()) + .subscribe { available -> newState { copy(callBlockerInstalled = available) } } view.activityResumed() - .map { callControl.isAvailable() } - .distinctUntilChanged() - .autoDisposable(view.scope()) - .subscribe { available -> newState { copy(callControlInstalled = available) } } + .map { callControl.isAvailable() } + .distinctUntilChanged() + .autoDispose(view.scope()) + .subscribe { available -> newState { copy(callControlInstalled = available) } } view.activityResumed() - .map { shouldIAnswer.isAvailable() } - .distinctUntilChanged() - .autoDisposable(view.scope()) - .subscribe { available -> newState { copy(siaInstalled = available) } } + .map { shouldIAnswer.isAvailable() } + .distinctUntilChanged() + .autoDispose(view.scope()) + .subscribe { available -> newState { copy(siaInstalled = available) } } view.qksmsClicked() - .observeOn(Schedulers.io()) - .map { getAddressesToBlock(qksms) } - .switchMap { numbers -> qksms.block(numbers).andThen(Observable.just(Unit)) } // Hack - .autoDisposable(view.scope()) - .subscribe { - prefs.blockingManager.set(Preferences.BLOCKING_MANAGER_QKSMS) - } + .observeOn(Schedulers.io()) + .map { getAddressesToBlock(qksms) } + .switchMap { numbers -> qksms.block(numbers).andThen(Observable.just(Unit)) } // Hack + .autoDispose(view.scope()) + .subscribe { + prefs.blockingManager.set(Preferences.BLOCKING_MANAGER_QKSMS) + } view.callBlockerClicked() - .filter { - val installed = callBlocker.isAvailable() - if (!installed) { - navigator.installCallBlocker() - } - - val enabled = prefs.blockingManager.get() == Preferences.BLOCKING_MANAGER_CB - installed && !enabled - } - .autoDisposable(view.scope()) - .subscribe { - prefs.blockingManager.set(Preferences.BLOCKING_MANAGER_CB) + .filter { + val installed = callBlocker.isAvailable() + if (!installed) { + navigator.installCallBlocker() } + val enabled = prefs.blockingManager.get() == Preferences.BLOCKING_MANAGER_CB + installed && !enabled + } + .autoDispose(view.scope()) + .subscribe { + prefs.blockingManager.set(Preferences.BLOCKING_MANAGER_CB) + } + view.callControlClicked() - .filter { - val installed = callControl.isAvailable() - if (!installed) { - navigator.installCallControl() - } - - val enabled = prefs.blockingManager.get() == Preferences.BLOCKING_MANAGER_CC - installed && !enabled - } - .observeOn(Schedulers.io()) - .map { getAddressesToBlock(callControl) } - .observeOn(AndroidSchedulers.mainThread()) - .switchMap { numbers -> - when (numbers.size) { - 0 -> Observable.just(true) - else -> view.showCopyDialog(context.getString(R.string.blocking_manager_call_control_title)) - .toObservable() - } + .filter { + val installed = callControl.isAvailable() + if (!installed) { + navigator.installCallControl() } - .doOnNext { newState { copy() } } // Radio button may have been selected when it shouldn't, fix it - .filter { it } - .observeOn(Schedulers.io()) - .map { getAddressesToBlock(callControl) } // This sucks. Can't wait to use coroutines - .switchMap { numbers -> callControl.block(numbers).andThen(Observable.just(Unit)) } // Hack - .autoDisposable(view.scope()) - .subscribe { - callControl.shouldBlock("callcontrol").blockingGet() - prefs.blockingManager.set(Preferences.BLOCKING_MANAGER_CC) + + val enabled = prefs.blockingManager.get() == Preferences.BLOCKING_MANAGER_CC + installed && !enabled + } + .observeOn(Schedulers.io()) + .map { getAddressesToBlock(callControl) } + .observeOn(AndroidSchedulers.mainThread()) + .switchMap { numbers -> + when (numbers.size) { + 0 -> Observable.just(true) + else -> view.showCopyDialog(context.getString(R.string.blocking_manager_call_control_title)) + .toObservable() } + } + .doOnNext { newState { copy() } } // Radio button may have been selected when it shouldn't, fix it + .filter { it } + .observeOn(Schedulers.io()) + .map { getAddressesToBlock(callControl) } // This sucks. Can't wait to use coroutines + .switchMap { numbers -> + callControl.block(numbers).andThen(Observable.just(Unit)) + } // Hack + .autoDispose(view.scope()) + .subscribe { + callControl.shouldBlock("callcontrol").blockingGet() + prefs.blockingManager.set(Preferences.BLOCKING_MANAGER_CC) + } view.siaClicked() - .filter { - val installed = shouldIAnswer.isAvailable() - if (!installed) { - navigator.installSia() - } - - val enabled = prefs.blockingManager.get() == Preferences.BLOCKING_MANAGER_SIA - installed && !enabled - } - .autoDisposable(view.scope()) - .subscribe { - prefs.blockingManager.set(Preferences.BLOCKING_MANAGER_SIA) + .filter { + val installed = shouldIAnswer.isAvailable() + if (!installed) { + navigator.installSia() } + + val enabled = prefs.blockingManager.get() == Preferences.BLOCKING_MANAGER_SIA + installed && !enabled + } + .autoDispose(view.scope()) + .subscribe { + prefs.blockingManager.set(Preferences.BLOCKING_MANAGER_SIA) + } } - private fun getAddressesToBlock(client: BlockingClient) = conversationRepo.getBlockedConversations() - .fold(listOf(), { numbers, conversation -> numbers + conversation.recipients.map { it.address } }) - .filter { number -> client.isBlacklisted(number).blockingGet() !is BlockingClient.Action.Block } + private fun getAddressesToBlock(client: BlockingClient) = + conversationRepo.getBlockedConversations() + .fold( + listOf(), + { numbers, conversation -> numbers + conversation.recipients.map { it.address } }) + .filter { number -> + client.isBlacklisted(number).blockingGet() !is BlockingClient.Action.Block + } } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/blocking/manager/BlockingManagerView.kt b/presentation/src/main/java/com/moez/QKSMS/feature/blocking/manager/BlockingManagerView.kt index 355a68b43..1ae9017c9 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/blocking/manager/BlockingManagerView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/blocking/manager/BlockingManagerView.kt @@ -1,8 +1,8 @@ package org.prauga.messages.feature.blocking.manager -import org.prauga.messages.common.base.QkViewContract import io.reactivex.Observable import io.reactivex.Single +import org.prauga.messages.common.base.QkViewContract interface BlockingManagerView : QkViewContract { diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/blocking/messages/BlockedMessagesPresenter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/blocking/messages/BlockedMessagesPresenter.kt index ca10adf3c..9b387897f 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/blocking/messages/BlockedMessagesPresenter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/blocking/messages/BlockedMessagesPresenter.kt @@ -20,6 +20,7 @@ package org.prauga.messages.feature.blocking.messages import com.uber.autodispose.android.lifecycle.scope import com.uber.autodispose.autoDisposable +import com.uber.autodispose.autoDispose import org.prauga.messages.R import org.prauga.messages.blocking.BlockingClient import org.prauga.messages.common.Navigator @@ -41,49 +42,50 @@ class BlockedMessagesPresenter @Inject constructor( super.bindIntents(view) view.menuReadyIntent - .autoDisposable(view.scope()) - .subscribe { newState { copy() } } + .autoDispose(view.scope()) + .subscribe { newState { copy() } } view.optionsItemIntent - .withLatestFrom(view.selectionChanges) { itemId, conversations -> - when (itemId) { - R.id.block -> { - view.showBlockingDialog(conversations, false) - view.clearSelection() - } - R.id.delete -> { - view.showDeleteDialog(conversations) - } + .withLatestFrom(view.selectionChanges) { itemId, conversations -> + when (itemId) { + R.id.block -> { + view.showBlockingDialog(conversations, false) + view.clearSelection() } + R.id.delete -> { + view.showDeleteDialog(conversations) + } } - .autoDisposable(view.scope()) - .subscribe() + + } + .autoDispose(view.scope()) + .subscribe() view.confirmDeleteIntent - .autoDisposable(view.scope()) - .subscribe { conversations -> - deleteConversations.execute(conversations) - view.clearSelection() - } + .autoDispose(view.scope()) + .subscribe { conversations -> + deleteConversations.execute(conversations) + view.clearSelection() + } view.conversationClicks - .autoDisposable(view.scope()) - .subscribe { threadId -> navigator.showConversation(threadId) } + .autoDispose(view.scope()) + .subscribe { threadId -> navigator.showConversation(threadId) } view.selectionChanges - .autoDisposable(view.scope()) - .subscribe { selection -> newState { copy(selected = selection.size) } } + .autoDispose(view.scope()) + .subscribe { selection -> newState { copy(selected = selection.size) } } view.backClicked - .withLatestFrom(state) { _, state -> - when (state.selected) { - 0 -> view.goBack() - else -> view.clearSelection() - } + .withLatestFrom(state) { _, state -> + when (state.selected) { + 0 -> view.goBack() + else -> view.clearSelection() } - .autoDisposable(view.scope()) - .subscribe() + } + .autoDispose(view.scope()) + .subscribe() } } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/blocking/numbers/BlockedNumbersPresenter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/blocking/numbers/BlockedNumbersPresenter.kt index b467460f4..364247d05 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/blocking/numbers/BlockedNumbersPresenter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/blocking/numbers/BlockedNumbersPresenter.kt @@ -20,6 +20,7 @@ package org.prauga.messages.feature.blocking.numbers import com.uber.autodispose.android.lifecycle.scope import com.uber.autodispose.autoDisposable +import com.uber.autodispose.autoDispose import org.prauga.messages.common.base.QkPresenter import org.prauga.messages.interactor.MarkUnblocked import org.prauga.messages.repository.BlockingRepository @@ -47,17 +48,17 @@ class BlockedNumbersPresenter @Inject constructor( } .doOnNext(blockingRepo::unblockNumber) .subscribeOn(Schedulers.io()) - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe() view.addAddress() - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe { view.showAddDialog() } view.saveAddress() .observeOn(Schedulers.io()) .subscribeOn(Schedulers.io()) - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe { address -> blockingRepo.blockNumber(address) } } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/BubbleUtils.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/BubbleUtils.kt index 6f1feb1cf..4c151c26f 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/BubbleUtils.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/BubbleUtils.kt @@ -33,7 +33,12 @@ object BubbleUtils { return message.compareSender(other) && diff < TIMESTAMP_THRESHOLD } - fun getBubble(emojiOnly: Boolean, canGroupWithPrevious: Boolean, canGroupWithNext: Boolean, isMe: Boolean): Int { + fun getBubble( + emojiOnly: Boolean, + canGroupWithPrevious: Boolean, + canGroupWithNext: Boolean, + isMe: Boolean + ): Int { return when { emojiOnly -> R.drawable.message_emoji !canGroupWithPrevious && canGroupWithNext -> if (isMe) R.drawable.message_out_first else R.drawable.message_in_first diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeActivity.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeActivity.kt index 0a4756878..ea4c7ef8e 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeActivity.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeActivity.kt @@ -27,21 +27,16 @@ import android.content.ContentValues import android.content.Intent import android.content.res.ColorStateList import android.net.Uri -import android.os.Build import android.os.Bundle import android.os.SystemClock import android.provider.ContactsContract import android.provider.MediaStore import android.text.format.DateFormat import android.view.ContextMenu -import android.view.DragEvent.ACTION_DRAG_ENDED -import android.view.DragEvent.ACTION_DRAG_EXITED -import android.view.DragEvent.ACTION_DROP import android.view.Menu import android.view.MenuItem import android.view.View import android.widget.SeekBar -import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.constraintlayout.widget.ConstraintSet import androidx.core.app.ActivityCompat @@ -56,8 +51,14 @@ import com.jakewharton.rxbinding2.widget.textChanges import com.moez.QKSMS.common.QkMediaPlayer import com.uber.autodispose.ObservableSubscribeProxy import com.uber.autodispose.android.lifecycle.scope -import com.uber.autodispose.autoDisposable +import com.uber.autodispose.autoDispose import dagger.android.AndroidInjection +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import io.reactivex.subjects.PublishSubject +import io.reactivex.subjects.Subject import org.prauga.messages.R import org.prauga.messages.common.Navigator import org.prauga.messages.common.base.QkThemedActivity @@ -65,7 +66,6 @@ import org.prauga.messages.common.util.DateFormatter import org.prauga.messages.common.util.extensions.autoScrollToStart import org.prauga.messages.common.util.extensions.dpToPx import org.prauga.messages.common.util.extensions.hideKeyboard -import org.prauga.messages.common.util.extensions.makeToast import org.prauga.messages.common.util.extensions.scrapViews import org.prauga.messages.common.util.extensions.setBackgroundTint import org.prauga.messages.common.util.extensions.setTint @@ -78,12 +78,6 @@ import org.prauga.messages.feature.compose.editing.ChipsAdapter import org.prauga.messages.feature.contacts.ContactsActivity import org.prauga.messages.model.Attachment import org.prauga.messages.model.Recipient -import io.reactivex.Observable -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.disposables.Disposable -import io.reactivex.schedulers.Schedulers -import io.reactivex.subjects.PublishSubject -import io.reactivex.subjects.Subject import java.text.SimpleDateFormat import java.util.Calendar import java.util.Date @@ -92,14 +86,21 @@ import java.util.concurrent.TimeUnit import javax.inject.Inject -class ComposeActivity : QkThemedActivity(ComposeActivityBinding::inflate), ComposeView { +class ComposeActivity : QkThemedActivity(ComposeActivityBinding::inflate), + ComposeView { - @Inject lateinit var composeAttachmentAdapter: ComposeAttachmentAdapter - @Inject lateinit var chipsAdapter: ChipsAdapter - @Inject lateinit var dateFormatter: DateFormatter - @Inject lateinit var messageAdapter: MessagesAdapter - @Inject lateinit var navigator: Navigator - @Inject lateinit var viewModelFactory: ViewModelProvider.Factory + @Inject + lateinit var composeAttachmentAdapter: ComposeAttachmentAdapter + @Inject + lateinit var chipsAdapter: ChipsAdapter + @Inject + lateinit var dateFormatter: DateFormatter + @Inject + lateinit var messageAdapter: MessagesAdapter + @Inject + lateinit var navigator: Navigator + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory override val activityVisibleIntent: Subject = PublishSubject.create() override val chipsSelectedIntent: Subject> = PublishSubject.create() @@ -117,19 +118,54 @@ class ComposeActivity : QkThemedActivity(ComposeActivity override val resendIntent: Subject by lazy { messageAdapter.resendClicks } override val attachmentDeletedIntent: Subject by lazy { composeAttachmentAdapter.attachmentDeleted } override val textChangedIntent by lazy { binding.message.textChanges() } - override val attachIntent by lazy { Observable.merge(binding.attach.clicks(), binding.shadeBackground.clicks()) } - override val cameraIntent by lazy { Observable.merge(binding.camera.clicks(), binding.cameraLabel.clicks()) } - override val attachImageFileIntent by lazy { Observable.merge(binding.gallery.clicks(), binding.galleryLabel.clicks()) } - override val attachAnyFileIntent by lazy { Observable.merge(binding.attachAFileIcon.clicks(), binding.attachAFileLabel.clicks()) } - override val scheduleIntent by lazy { Observable.merge(binding.schedule.clicks(), binding.scheduleLabel.clicks()) } - override val attachContactIntent by lazy { Observable.merge(binding.contact.clicks(), binding.contactLabel.clicks()) } + override val attachIntent by lazy { + Observable.merge( + binding.attach.clicks(), + binding.shadeBackground.clicks() + ) + } + override val cameraIntent by lazy { + Observable.merge( + binding.camera.clicks(), + binding.cameraLabel.clicks() + ) + } + override val attachImageFileIntent by lazy { + Observable.merge( + binding.gallery.clicks(), + binding.galleryLabel.clicks() + ) + } + override val attachAnyFileIntent by lazy { + Observable.merge( + binding.attachAFileIcon.clicks(), + binding.attachAFileLabel.clicks() + ) + } + override val scheduleIntent by lazy { + Observable.merge( + binding.schedule.clicks(), + binding.scheduleLabel.clicks() + ) + } + override val attachContactIntent by lazy { + Observable.merge( + binding.contact.clicks(), + binding.contactLabel.clicks() + ) + } override val attachAnyFileSelectedIntent: Subject = PublishSubject.create() override val contactSelectedIntent: Subject = PublishSubject.create() override val inputContentIntent by lazy { binding.message.inputContentSelected } override val scheduleSelectedIntent: Subject = PublishSubject.create() override val changeSimIntent by lazy { binding.sim.clicks() } override val scheduleCancelIntent by lazy { binding.scheduledCancel.clicks() } - override val sendIntent by lazy { Observable.merge(binding.send.clicks(), binding.scheduledSend.clicks()) } + override val sendIntent by lazy { + Observable.merge( + binding.send.clicks(), + binding.scheduledSend.clicks() + ) + } override val viewQksmsPlusIntent: Subject = PublishSubject.create() override val backPressedIntent: Subject = PublishSubject.create() override val confirmDeleteIntent: Subject> = PublishSubject.create() @@ -138,14 +174,18 @@ class ComposeActivity : QkThemedActivity(ComposeActivity override val shadeIntent by lazy { binding.shadeBackground.clicks() } override val recordAudioStartStopRecording: Subject = PublishSubject.create() override val recordAnAudioMessage by lazy { - Observable.merge(binding.recordAudioMsg.clicks(), + Observable.merge( + binding.recordAudioMsg.clicks(), binding.attachAnAudioMessageIcon.clicks(), - binding.attachAnAudioMessageLabel.clicks()) + binding.attachAnAudioMessageLabel.clicks() + ) } override val recordAudioAbort by lazy { binding.audioMsgAbort.clicks() } override val recordAudioAttach by lazy { binding.audioMsgAttach.clicks() } - override val recordAudioPlayerPlayPause: Subject = PublishSubject.create() - override val recordAudioPlayerConfigUI: Subject = PublishSubject.create() + override val recordAudioPlayerPlayPause: Subject = + PublishSubject.create() + override val recordAudioPlayerConfigUI: Subject = + PublishSubject.create() override val recordAudioPlayerVisible: Subject = PublishSubject.create() override val recordAudioMsgRecordVisible: Subject = PublishSubject.create() override val recordAudioChronometer: Subject = PublishSubject.create() @@ -153,7 +193,12 @@ class ComposeActivity : QkThemedActivity(ComposeActivity private var seekBarUpdater: Disposable? = null - private val viewModel by lazy { ViewModelProviders.of(this, viewModelFactory)[ComposeViewModel::class.java] } + private val viewModel by lazy { + ViewModelProviders.of( + this, + viewModelFactory + )[ComposeViewModel::class.java] + } private var cameraDestination: Uri? = null private var isSelection = false @@ -162,7 +207,7 @@ class ComposeActivity : QkThemedActivity(ComposeActivity return Observable.interval(500, TimeUnit.MILLISECONDS) .subscribeOn(Schedulers.single()) .observeOn(AndroidSchedulers.mainThread()) - .autoDisposable(scope()) + .autoDispose(scope()) } override fun onCreate(savedInstanceState: Bundle?) { @@ -174,153 +219,162 @@ class ComposeActivity : QkThemedActivity(ComposeActivity binding.contentView.layoutTransition = LayoutTransition().apply { disableTransitionType(LayoutTransition.CHANGING) } - chipsAdapter.view = binding.chips - - binding.chips.itemAnimator = null - binding.chips.layoutManager = FlexboxLayoutManager(this) - - messageAdapter.autoScrollToStart(binding.messageList) - messageAdapter.emptyView = binding.messagesEmpty - - binding.messageList.setHasFixedSize(true) - binding.messageList.adapter = messageAdapter - - binding.messageAttachments.adapter = composeAttachmentAdapter - - binding.message.supportsInputContent = true - - theme - .doOnNext { - binding.loading.setTint(it.theme) - - // entire attach menu - binding.attach.setBackgroundTint(it.theme); binding.attach.setTint(it.textPrimary) - binding.contact.setBackgroundTint(it.theme); binding.contact.setTint(it.textPrimary) - binding.contactLabel.setBackgroundTint(it.theme); binding.contactLabel.setTint(it.textPrimary) - binding.schedule.setBackgroundTint(it.theme); binding.schedule.setTint(it.textPrimary) - binding.scheduleLabel.setBackgroundTint(it.theme); binding.scheduleLabel.setTint(it.textPrimary) - binding.attachAFileIcon.setBackgroundTint(it.theme); binding.attachAFileIcon.setTint(it.textPrimary) - binding.attachAFileLabel.setBackgroundTint(it.theme); binding.attachAFileLabel.setTint(it.textPrimary) - binding.attachAnAudioMessageIcon.setBackgroundTint(it.theme); binding.attachAnAudioMessageIcon.setTint(it.textPrimary) - binding.attachAnAudioMessageLabel.setBackgroundTint(it.theme); binding.attachAnAudioMessageLabel.setTint(it.textPrimary) - binding.gallery.setBackgroundTint(it.theme); binding.gallery.setTint(it.textPrimary) - binding.galleryLabel.setBackgroundTint(it.theme); binding.galleryLabel.setTint(it.textPrimary) - binding.camera.setBackgroundTint(it.theme); binding.camera.setTint(it.textPrimary) - binding.cameraLabel.setBackgroundTint(it.theme); binding.cameraLabel.setTint(it.textPrimary) - - // audio message recording - binding.audioMsgRecord.setColor(it.theme) - binding.audioMsgPlayerPlayPause.setTint(it.theme) - binding.audioMsgPlayerSeekBar.apply { - thumbTintList = ColorStateList.valueOf(it.theme) - progressBackgroundTintList = ColorStateList.valueOf(it.theme) - progressTintList = ColorStateList.valueOf(it.theme) - } + chipsAdapter.view = binding.chips - messageAdapter.theme = it - } - .autoDisposable(scope()) - .subscribe() - - // context menu registration for message parts - messagePartContextMenuRegistrar - .mapNotNull { it } - .autoDisposable(scope()) - .subscribe { registerForContextMenu(it) } - - // start/stop audio message recording - binding.audioMsgRecord.setOnClickListener { - recordAudioRecord.onNext(binding.audioMsgRecord.getState()) - } + binding.chips.itemAnimator = null + binding.chips.layoutManager = FlexboxLayoutManager(this) - recordAudioChronometer - .subscribeOn(AndroidSchedulers.mainThread()) - .distinctUntilChanged() - .autoDisposable(scope()) - .subscribe { - if (it) { - binding.audioMsgDuration.base = SystemClock.elapsedRealtime() - binding.audioMsgDuration.start() - } else { - binding.audioMsgDuration.stop() - } + messageAdapter.autoScrollToStart(binding.messageList) + messageAdapter.emptyView = binding.messagesEmpty + + binding.messageList.setHasFixedSize(true) + binding.messageList.adapter = messageAdapter + + binding.messageAttachments.adapter = composeAttachmentAdapter + + binding.message.supportsInputContent = true + + theme + .doOnNext { + binding.loading.setTint(it.theme) + + // entire attach menu + binding.attach.setBackgroundTint(it.theme); binding.attach.setTint(it.textPrimary) + binding.contact.setBackgroundTint(it.theme); binding.contact.setTint(it.textPrimary) + binding.contactLabel.setBackgroundTint(it.theme); binding.contactLabel.setTint(it.textPrimary) + binding.schedule.setBackgroundTint(it.theme); binding.schedule.setTint(it.textPrimary) + binding.scheduleLabel.setBackgroundTint(it.theme); binding.scheduleLabel.setTint(it.textPrimary) + binding.attachAFileIcon.setBackgroundTint(it.theme); binding.attachAFileIcon.setTint( + it.textPrimary + ) + binding.attachAFileLabel.setBackgroundTint(it.theme); binding.attachAFileLabel.setTint( + it.textPrimary + ) + binding.attachAnAudioMessageIcon.setBackgroundTint(it.theme); binding.attachAnAudioMessageIcon.setTint( + it.textPrimary + ) + binding.attachAnAudioMessageLabel.setBackgroundTint(it.theme); binding.attachAnAudioMessageLabel.setTint( + it.textPrimary + ) + binding.gallery.setBackgroundTint(it.theme); binding.gallery.setTint(it.textPrimary) + binding.galleryLabel.setBackgroundTint(it.theme); binding.galleryLabel.setTint(it.textPrimary) + binding.camera.setBackgroundTint(it.theme); binding.camera.setTint(it.textPrimary) + binding.cameraLabel.setBackgroundTint(it.theme); binding.cameraLabel.setTint(it.textPrimary) + + // audio message recording + binding.audioMsgRecord.setColor(it.theme) + binding.audioMsgPlayerPlayPause.setTint(it.theme) + binding.audioMsgPlayerSeekBar.apply { + thumbTintList = ColorStateList.valueOf(it.theme) + progressBackgroundTintList = ColorStateList.valueOf(it.theme) + progressTintList = ColorStateList.valueOf(it.theme) } - // audio record playback play/pause button - binding.audioMsgPlayerPlayPause.setOnClickListener { - recordAudioPlayerPlayPause.onNext( - binding.audioMsgPlayerPlayPause.tag as QkMediaPlayer.PlayingState - ) + messageAdapter.theme = it } + .autoDispose(scope()) + .subscribe() + + // context menu registration for message parts + messagePartContextMenuRegistrar + .mapNotNull { it } + .autoDispose(scope()) + .subscribe { registerForContextMenu(it) } + + // start/stop audio message recording + binding.audioMsgRecord.setOnClickListener { + recordAudioRecord.onNext(binding.audioMsgRecord.getState()) + } - recordAudioMsgRecordVisible - .subscribeOn(AndroidSchedulers.mainThread()) - .distinctUntilChanged() - .autoDisposable(scope()) - .subscribe { - binding.audioMsgRecord.isVisible = it - binding.audioMsgDuration.isVisible = - it // chronometer follows record button visibility - binding.audioMsgBluetooth.isVisible = !it + recordAudioChronometer + .subscribeOn(AndroidSchedulers.mainThread()) + .distinctUntilChanged() + .autoDispose(scope()) + .subscribe { + if (it) { + binding.audioMsgDuration.base = SystemClock.elapsedRealtime() + binding.audioMsgDuration.start() + } else { + binding.audioMsgDuration.stop() } + } - recordAudioPlayerVisible - .subscribeOn(AndroidSchedulers.mainThread()) - .distinctUntilChanged() - .autoDisposable(scope()) - .subscribe { - binding.audioMsgPlayerBackground.isVisible = it - recordAudioPlayerConfigUI.onNext(QkMediaPlayer.PlayingState.Stopped) - } + // audio record playback play/pause button + binding.audioMsgPlayerPlayPause.setOnClickListener { + recordAudioPlayerPlayPause.onNext( + binding.audioMsgPlayerPlayPause.tag as QkMediaPlayer.PlayingState + ) + } - recordAudioPlayerConfigUI - .subscribeOn(AndroidSchedulers.mainThread()) - .distinctUntilChanged() - .autoDisposable(scope()) - .subscribe { - when (it) { - QkMediaPlayer.PlayingState.Playing -> { - binding.audioMsgPlayerPlayPause.tag = QkMediaPlayer.PlayingState.Playing - QkMediaPlayer.start() - binding.audioMsgPlayerPlayPause.setImageResource(R.drawable.exo_icon_pause) - seekBarUpdater = getSeekBarUpdater().subscribe { - binding.audioMsgPlayerSeekBar.progress = QkMediaPlayer.currentPosition - binding.audioMsgPlayerSeekBar.max = QkMediaPlayer.duration - } - binding.audioMsgPlayerSeekBar.isEnabled = true - } + recordAudioMsgRecordVisible + .subscribeOn(AndroidSchedulers.mainThread()) + .distinctUntilChanged() + .autoDispose(scope()) + .subscribe { + binding.audioMsgRecord.isVisible = it + binding.audioMsgDuration.isVisible = + it // chronometer follows record button visibility + binding.audioMsgBluetooth.isVisible = !it + } - QkMediaPlayer.PlayingState.Paused -> { - binding.audioMsgPlayerPlayPause.tag = QkMediaPlayer.PlayingState.Paused - QkMediaPlayer.pause() - binding.audioMsgPlayerPlayPause.setImageResource(R.drawable.exo_icon_play) - seekBarUpdater?.dispose() - } + recordAudioPlayerVisible + .subscribeOn(AndroidSchedulers.mainThread()) + .distinctUntilChanged() + .autoDispose(scope()) + .subscribe { + binding.audioMsgPlayerBackground.isVisible = it + recordAudioPlayerConfigUI.onNext(QkMediaPlayer.PlayingState.Stopped) + } - else -> { - binding.audioMsgPlayerPlayPause.tag = QkMediaPlayer.PlayingState.Stopped - QkMediaPlayer.reset() - binding.audioMsgPlayerPlayPause.setImageResource(R.drawable.exo_icon_play) - seekBarUpdater?.dispose() - binding.audioMsgPlayerSeekBar.progress = 0 - binding.audioMsgPlayerSeekBar.isEnabled = false + recordAudioPlayerConfigUI + .subscribeOn(AndroidSchedulers.mainThread()) + .distinctUntilChanged() + .autoDispose(scope()) + .subscribe { + when (it) { + QkMediaPlayer.PlayingState.Playing -> { + binding.audioMsgPlayerPlayPause.tag = QkMediaPlayer.PlayingState.Playing + QkMediaPlayer.start() + binding.audioMsgPlayerPlayPause.setImageResource(R.drawable.exo_icon_pause) + seekBarUpdater = getSeekBarUpdater().subscribe { + binding.audioMsgPlayerSeekBar.progress = QkMediaPlayer.currentPosition + binding.audioMsgPlayerSeekBar.max = QkMediaPlayer.duration } + binding.audioMsgPlayerSeekBar.isEnabled = true } - } - // audio msg player seek bar handler - binding.audioMsgPlayerSeekBar.setOnSeekBarChangeListener( - object : SeekBar.OnSeekBarChangeListener { - override fun onProgressChanged(p0: SeekBar?, progress: Int, fromUser: Boolean) { - // if seek was initiated by the user and this part is currently playing - if (fromUser) - QkMediaPlayer.seekTo(progress) + + QkMediaPlayer.PlayingState.Paused -> { + binding.audioMsgPlayerPlayPause.tag = QkMediaPlayer.PlayingState.Paused + QkMediaPlayer.pause() + binding.audioMsgPlayerPlayPause.setImageResource(R.drawable.exo_icon_play) + seekBarUpdater?.dispose() + } + + else -> { + binding.audioMsgPlayerPlayPause.tag = QkMediaPlayer.PlayingState.Stopped + QkMediaPlayer.reset() + binding.audioMsgPlayerPlayPause.setImageResource(R.drawable.exo_icon_play) + seekBarUpdater?.dispose() + binding.audioMsgPlayerSeekBar.progress = 0 + binding.audioMsgPlayerSeekBar.isEnabled = false } - override fun onStartTrackingTouch(p0: SeekBar?) {} - override fun onStopTrackingTouch(p0: SeekBar?) {} } - ) + } + // audio msg player seek bar handler + binding.audioMsgPlayerSeekBar.setOnSeekBarChangeListener( + object : SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged(p0: SeekBar?, progress: Int, fromUser: Boolean) { + // if seek was initiated by the user and this part is currently playing + if (fromUser) + QkMediaPlayer.seekTo(progress) + } + + override fun onStartTrackingTouch(p0: SeekBar?) {} + override fun onStopTrackingTouch(p0: SeekBar?) {} + } + ) - window.callback = ComposeWindowCallback(window.callback, this) + window.callback = ComposeWindowCallback(window.callback, this) } override fun onStart() { @@ -352,14 +406,20 @@ class ComposeActivity : QkThemedActivity(ComposeActivity threadId.onNext(state.threadId) title = when { - state.selectedMessages > 0 -> getString(R.string.compose_title_selected, state.selectedMessages) + state.selectedMessages > 0 -> getString( + R.string.compose_title_selected, + state.selectedMessages + ) + state.query.isNotEmpty() -> state.query else -> state.conversationtitle } binding.toolbarSubtitle.setVisible(state.query.isNotEmpty()) - binding.toolbarSubtitle.text = getString(R.string.compose_subtitle_results, state.searchSelectionPosition, - state.searchResults) + binding.toolbarSubtitle.text = getString( + R.string.compose_subtitle_results, state.searchSelectionPosition, + state.searchResults + ) binding.toolbarTitle.setVisible(!state.editingMode) binding.chips.setVisible(state.editingMode) @@ -368,25 +428,36 @@ class ComposeActivity : QkThemedActivity(ComposeActivity // Don't set the adapters unless needed if (state.editingMode && binding.chips.adapter == null) binding.chips.adapter = chipsAdapter - binding.toolbar.menu.findItem(R.id.viewScheduledMessages)?.isVisible = !state.editingMode && state.selectedMessages == 0 - && state.query.isEmpty() && state.hasScheduledMessages - binding.toolbar.menu.findItem(R.id.select_all)?.isVisible = !state.editingMode && (messageAdapter.itemCount > 1) && state.selectedMessages != 0 + binding.toolbar.menu.findItem(R.id.viewScheduledMessages)?.isVisible = + !state.editingMode && state.selectedMessages == 0 + && state.query.isEmpty() && state.hasScheduledMessages + binding.toolbar.menu.findItem(R.id.select_all)?.isVisible = + !state.editingMode && (messageAdapter.itemCount > 1) && state.selectedMessages != 0 binding.toolbar.menu.findItem(R.id.add)?.isVisible = state.editingMode - binding.toolbar.menu.findItem(R.id.call)?.isVisible = !state.editingMode && state.selectedMessages == 0 - && state.query.isEmpty() - binding.toolbar.menu.findItem(R.id.info)?.isVisible = !state.editingMode && state.selectedMessages == 0 - && state.query.isEmpty() + binding.toolbar.menu.findItem(R.id.call)?.isVisible = + !state.editingMode && state.selectedMessages == 0 + && state.query.isEmpty() + binding.toolbar.menu.findItem(R.id.info)?.isVisible = + !state.editingMode && state.selectedMessages == 0 + && state.query.isEmpty() binding.toolbar.menu.findItem(R.id.copy)?.isVisible = !state.editingMode && state.selectedMessages > 0 && state.selectedMessagesHaveText binding.toolbar.menu.findItem(R.id.share)?.isVisible = !state.editingMode && state.selectedMessages > 0 && state.selectedMessagesHaveText - binding.toolbar.menu.findItem(R.id.details)?.isVisible = !state.editingMode && state.selectedMessages == 1 - binding.toolbar.menu.findItem(R.id.delete)?.isVisible = !state.editingMode && ((state.selectedMessages > 0) || state.canSend) - binding.toolbar.menu.findItem(R.id.forward)?.isVisible = !state.editingMode && state.selectedMessages == 1 - binding.toolbar.menu.findItem(R.id.show_status)?.isVisible = !state.editingMode && state.selectedMessages > 0 - binding.toolbar.menu.findItem(R.id.previous)?.isVisible = state.selectedMessages == 0 && state.query.isNotEmpty() - binding.toolbar.menu.findItem(R.id.next)?.isVisible = state.selectedMessages == 0 && state.query.isNotEmpty() - binding.toolbar.menu.findItem(R.id.clear)?.isVisible = state.selectedMessages == 0 && state.query.isNotEmpty() + binding.toolbar.menu.findItem(R.id.details)?.isVisible = + !state.editingMode && state.selectedMessages == 1 + binding.toolbar.menu.findItem(R.id.delete)?.isVisible = + !state.editingMode && ((state.selectedMessages > 0) || state.canSend) + binding.toolbar.menu.findItem(R.id.forward)?.isVisible = + !state.editingMode && state.selectedMessages == 1 + binding.toolbar.menu.findItem(R.id.show_status)?.isVisible = + !state.editingMode && state.selectedMessages > 0 + binding.toolbar.menu.findItem(R.id.previous)?.isVisible = + state.selectedMessages == 0 && state.query.isNotEmpty() + binding.toolbar.menu.findItem(R.id.next)?.isVisible = + state.selectedMessages == 0 && state.query.isNotEmpty() + binding.toolbar.menu.findItem(R.id.clear)?.isVisible = + state.selectedMessages == 0 && state.query.isNotEmpty() chipsAdapter.data = state.selectedChips @@ -420,7 +491,7 @@ class ComposeActivity : QkThemedActivity(ComposeActivity elevation = 5.dpToPx(context).toFloat() // above attach menu } - else-> visibility = View.GONE + else -> visibility = View.GONE } } @@ -431,13 +502,17 @@ class ComposeActivity : QkThemedActivity(ComposeActivity binding.counter.setVisible(binding.counter.text.isNotBlank()) binding.sim.setVisible(state.subscription != null) - binding.sim.contentDescription = getString(R.string.compose_sim_cd, state.subscription?.displayName) + binding.sim.contentDescription = + getString(R.string.compose_sim_cd, state.subscription?.displayName) binding.simIndex.text = state.subscription?.simSlotIndex?.plus(1)?.toString() // show either send, audio msg record, or sendScheduled button - binding.send.visibility = if (state.canSend && !state.loading && state.scheduled == 0L) View.VISIBLE else View.INVISIBLE - binding.recordAudioMsg.visibility = if (state.canSend && !state.loading) View.INVISIBLE else View.VISIBLE - binding.scheduledSend.visibility = if (state.canSend && (state.scheduled != 0L) && !state.loading) View.VISIBLE else View.INVISIBLE + binding.send.visibility = + if (state.canSend && !state.loading && state.scheduled == 0L) View.VISIBLE else View.INVISIBLE + binding.recordAudioMsg.visibility = + if (state.canSend && !state.loading) View.INVISIBLE else View.VISIBLE + binding.scheduledSend.visibility = + if (state.canSend && (state.scheduled != 0L) && !state.loading) View.VISIBLE else View.INVISIBLE // if not in editing mode, and there are no non-me participants that can be sent to, // hide controls that allow constructing a reply and inform user no valid recipients @@ -507,7 +582,11 @@ class ComposeActivity : QkThemedActivity(ComposeActivity } override fun requestStoragePermission() { - ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 0) + ActivityCompat.requestPermissions( + this, + arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), + 0 + ) } override fun requestRecordAudioPermission() { @@ -515,24 +594,39 @@ class ComposeActivity : QkThemedActivity(ComposeActivity } override fun requestSmsPermission() { - ActivityCompat.requestPermissions(this, arrayOf( - Manifest.permission.READ_SMS, - Manifest.permission.SEND_SMS), 0) + ActivityCompat.requestPermissions( + this, arrayOf( + Manifest.permission.READ_SMS, + Manifest.permission.SEND_SMS + ), 0 + ) } override fun requestDatePicker() { val calendar = Calendar.getInstance() - DatePickerDialog(this, DatePickerDialog.OnDateSetListener { _, year, month, day -> - TimePickerDialog(this, TimePickerDialog.OnTimeSetListener { _, hour, minute -> - calendar.set(Calendar.YEAR, year) - calendar.set(Calendar.MONTH, month) - calendar.set(Calendar.DAY_OF_MONTH, day) - calendar.set(Calendar.HOUR_OF_DAY, hour) - calendar.set(Calendar.MINUTE, minute) - scheduleSelectedIntent.onNext(calendar.timeInMillis) - }, calendar.get(Calendar.HOUR_OF_DAY), calendar.get(Calendar.MINUTE), DateFormat.is24HourFormat(this)) - .show() - }, calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH)).show() + DatePickerDialog( + this, + DatePickerDialog.OnDateSetListener { _, year, month, day -> + TimePickerDialog( + this, + TimePickerDialog.OnTimeSetListener { _, hour, minute -> + calendar.set(Calendar.YEAR, year) + calendar.set(Calendar.MONTH, month) + calendar.set(Calendar.DAY_OF_MONTH, day) + calendar.set(Calendar.HOUR_OF_DAY, hour) + calendar.set(Calendar.MINUTE, minute) + scheduleSelectedIntent.onNext(calendar.timeInMillis) + }, + calendar.get(Calendar.HOUR_OF_DAY), + calendar.get(Calendar.MINUTE), + DateFormat.is24HourFormat(this) + ) + .show() + }, + calendar.get(Calendar.YEAR), + calendar.get(Calendar.MONTH), + calendar.get(Calendar.DAY_OF_MONTH) + ).show() // On some devices, the keyboard can cover the date picker binding.message.hideKeyboard() @@ -542,14 +636,18 @@ class ComposeActivity : QkThemedActivity(ComposeActivity val intent = Intent(Intent.ACTION_PICK) .setType(ContactsContract.Contacts.CONTENT_TYPE) - startActivityForResult(Intent.createChooser(intent, null), ComposeView.AttachContactRequestCode) + startActivityForResult( + Intent.createChooser(intent, null), + ComposeView.AttachContactRequestCode + ) } override fun showContacts(sharing: Boolean, chips: List) { binding.message.hideKeyboard() // Track if this is the initial contact selection (no existing chips) isSelection = chips.isEmpty() - val serialized = HashMap(chips.associate { chip -> chip.address to chip.contact?.lookupKey }) + val serialized = + HashMap(chips.associate { chip -> chip.address to chip.contact?.lookupKey }) val intent = Intent(this, ContactsActivity::class.java) .putExtra(ContactsActivity.SharingKey, sharing) .putExtra(ContactsActivity.ChipsKey, serialized) @@ -568,7 +666,14 @@ class ComposeActivity : QkThemedActivity(ComposeActivity override fun requestCamera() { cameraDestination = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) - .let { timestamp -> ContentValues().apply { put(MediaStore.Images.Media.TITLE, timestamp) } } + .let { timestamp -> + ContentValues().apply { + put( + MediaStore.Images.Media.TITLE, + timestamp + ) + } + } .let { cv -> contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, cv) } val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeActivityModule.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeActivityModule.kt index 25b1ae2f6..fd8c99ee5 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeActivityModule.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeActivityModule.kt @@ -33,11 +33,13 @@ class ComposeActivityModule { @Provides @Named("query") - fun provideQuery(activity: ComposeActivity): String = activity.intent.extras?.getString("query") ?: "" + fun provideQuery(activity: ComposeActivity): String = + activity.intent.extras?.getString("query") ?: "" @Provides @Named("threadId") - fun provideThreadId(activity: ComposeActivity): Long = activity.intent.extras?.getLong("threadId") ?: 0L + fun provideThreadId(activity: ComposeActivity): Long = + activity.intent.extras?.getLong("threadId") ?: 0L @Provides @Named("addresses") @@ -66,7 +68,8 @@ class ComposeActivityModule { retVal.append( activity.intent?.extras?.getString(Intent.EXTRA_TEXT) ?: activity.intent?.extras?.getString("sms_body") - ?: "") + ?: "" + ) // from body param value(s) if intent data uri is like // sms:12345678?body=hello%20there&body=goodbye diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeAttachmentAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeAttachmentAdapter.kt index 19aaeb823..93eef6943 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeAttachmentAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeAttachmentAdapter.kt @@ -23,6 +23,9 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.view.isVisible +import ezvcard.Ezvcard +import io.reactivex.subjects.PublishSubject +import io.reactivex.subjects.Subject import org.prauga.messages.R import org.prauga.messages.common.base.QkAdapter import org.prauga.messages.common.base.QkViewHolder @@ -33,9 +36,6 @@ import org.prauga.messages.extensions.getName import org.prauga.messages.feature.extensions.LoadBestIconIntoImageView import org.prauga.messages.feature.extensions.loadBestIconIntoImageView import org.prauga.messages.model.Attachment -import ezvcard.Ezvcard -import io.reactivex.subjects.PublishSubject -import io.reactivex.subjects.Subject import javax.inject.Inject @@ -96,6 +96,7 @@ class ComposeAttachmentAdapter @Inject constructor( binding.fileName.text = context.getString(R.string.attachment_missing) binding.fileName.visibility = View.VISIBLE } + LoadBestIconIntoImageView.ActivityIcon, LoadBestIconIntoImageView.DefaultAudioIcon, LoadBestIconIntoImageView.GenericIcon -> { @@ -103,6 +104,7 @@ class ComposeAttachmentAdapter @Inject constructor( binding.fileName.text = attachment.uri.getName(context) binding.fileName.visibility = View.VISIBLE } + else -> binding.fileName.visibility = View.GONE } } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeState.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeState.kt index e63a2ac6f..062e51449 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeState.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeState.kt @@ -18,12 +18,12 @@ */ package org.prauga.messages.feature.compose +import io.realm.RealmResults import org.prauga.messages.compat.SubscriptionInfoCompat import org.prauga.messages.model.Attachment import org.prauga.messages.model.Conversation import org.prauga.messages.model.Message import org.prauga.messages.model.Recipient -import io.realm.RealmResults data class ComposeState( val hasError: Boolean = false, diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeView.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeView.kt index f2a2b0a59..52c69713f 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeView.kt @@ -24,12 +24,12 @@ import android.view.View import androidx.annotation.StringRes import androidx.core.view.inputmethod.InputContentInfoCompat import com.moez.QKSMS.common.QkMediaPlayer +import io.reactivex.Observable +import io.reactivex.subjects.Subject import org.prauga.messages.common.base.QkView import org.prauga.messages.common.widget.MicInputCloudView import org.prauga.messages.model.Attachment import org.prauga.messages.model.Recipient -import io.reactivex.Observable -import io.reactivex.subjects.Subject interface ComposeView : QkView { @@ -107,7 +107,7 @@ interface ComposeView : QkView { fun setDraft(draft: String) fun scrollToMessage(id: Long) fun showQksmsPlusSnackbar(@StringRes message: Int) - fun showDeleteDialog( messages: List) + fun showDeleteDialog(messages: List) fun showClearCurrentMessageDialog() fun focusMessage() } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeViewModel.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeViewModel.kt index 814178b5d..4cf4f37f7 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeViewModel.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/ComposeViewModel.kt @@ -38,7 +38,17 @@ import com.moez.QKSMS.manager.MediaRecorderManager.AUDIO_FILE_PREFIX import com.moez.QKSMS.manager.MediaRecorderManager.AUDIO_FILE_SUFFIX import com.moez.QKSMS.util.Constants.Companion.SAVED_MESSAGE_TEXT_FILE_PREFIX import com.uber.autodispose.android.lifecycle.scope -import com.uber.autodispose.autoDisposable +import com.uber.autodispose.autoDispose +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.functions.BiFunction +import io.reactivex.rxkotlin.Observables +import io.reactivex.rxkotlin.plusAssign +import io.reactivex.rxkotlin.withLatestFrom +import io.reactivex.schedulers.Schedulers +import io.reactivex.subjects.BehaviorSubject +import io.reactivex.subjects.PublishSubject +import io.reactivex.subjects.Subject import org.prauga.messages.R import org.prauga.messages.common.Navigator import org.prauga.messages.common.base.QkViewModel @@ -78,15 +88,6 @@ import org.prauga.messages.util.FileUtils import org.prauga.messages.util.PhoneNumberUtils import org.prauga.messages.util.Preferences import org.prauga.messages.util.tryOrNull -import io.reactivex.Observable -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.rxkotlin.Observables -import io.reactivex.rxkotlin.plusAssign -import io.reactivex.rxkotlin.withLatestFrom -import io.reactivex.schedulers.Schedulers -import io.reactivex.subjects.BehaviorSubject -import io.reactivex.subjects.PublishSubject -import io.reactivex.subjects.Subject import timber.log.Timber import java.text.SimpleDateFormat import java.util.Locale @@ -124,12 +125,15 @@ class ComposeViewModel @Inject constructor( private val sendMessage: SendMessage, private val subscriptionManager: SubscriptionManagerCompat, private val saveImage: SaveImage, -) : QkViewModel(ComposeState( +) : QkViewModel( + ComposeState( editingMode = threadId == 0L && addresses.isEmpty(), threadId = threadId, - query = query) + query = query + ) ) { - private val chipsReducer: Subject<(List) -> List> = PublishSubject.create() + private val chipsReducer: Subject<(List) -> List> = + PublishSubject.create() private val conversation: Subject = BehaviorSubject.create() private val messages: Subject> = BehaviorSubject.create() private val selectedChips: Subject> = BehaviorSubject.createDefault(listOf()) @@ -145,11 +149,11 @@ class ComposeViewModel @Inject constructor( // set shared subscription into state if set subscriptionManager.activeSubscriptionInfoList.firstOrNull { it.subscriptionId == sharedSubscriptionId - }?.let { newState { copy(subscription = it)} } + }?.let { newState { copy(subscription = it) } } // set shared scheduled datetime into state if set if (sharedScheduledDateTime != 0L) - newState { copy (scheduled = sharedScheduledDateTime) } + newState { copy(scheduled = sharedScheduledDateTime) } // set shared sendAsGroup into state if set if (sharedSendAsGroup != null) @@ -179,7 +183,7 @@ class ComposeViewModel @Inject constructor( .switchMap { conversationId -> conversationRepo.getConversationAsync(conversationId).asObservable() } - } + } // Merges two potential conversation sources (constructor threadId and contact selection) // into a single stream of conversations. If the conversation was deleted, notify the @@ -194,42 +198,47 @@ class ComposeViewModel @Inject constructor( selectedChips.onNext(addresses.map { address -> Recipient(address = address) }) disposables += chipsReducer - .scan(listOf()) { previousState, reducer -> reducer(previousState) } - .doOnNext { chips -> newState { copy(selectedChips = chips) } } - .skipUntil(state.filter { state -> state.editingMode }) - .takeUntil(state.filter { state -> !state.editingMode }) - .subscribe(selectedChips::onNext) + .scan(listOf()) { previousState, reducer -> reducer(previousState) } + .doOnNext { chips -> newState { copy(selectedChips = chips) } } + .skipUntil(state.filter { state -> state.editingMode }) + .takeUntil(state.filter { state -> !state.editingMode }) + .subscribe(selectedChips::onNext) // When the conversation changes, mark read, and update the recipientId and the messages for the adapter disposables += conversation - .distinctUntilChanged { conversation -> conversation.id } - .observeOn(AndroidSchedulers.mainThread()) - .map { conversation -> - val messages = messageRepo.getMessages(conversation.id) - newState { copy(threadId = conversation.id, messages = Pair(conversation, messages)) } - messages + .distinctUntilChanged { conversation -> conversation.id } + .observeOn(AndroidSchedulers.mainThread()) + .map { conversation -> + val messages = messageRepo.getMessages(conversation.id) + newState { + copy( + threadId = conversation.id, + messages = Pair(conversation, messages) + ) } - .switchMap { messages -> messages.asObservable() } - .subscribe(messages::onNext) + messages + } + .switchMap { messages -> messages.asObservable() } + .subscribe(messages::onNext) disposables += conversation - .map { conversation -> conversation.getTitle() } - .distinctUntilChanged() - .subscribe { title -> newState { copy(conversationtitle = title) } } + .map { conversation -> conversation.getTitle() } + .distinctUntilChanged() + .subscribe { title -> newState { copy(conversationtitle = title) } } disposables += prefs.sendAsGroup.asObservable() - .distinctUntilChanged() - .subscribe { enabled -> newState { copy(sendAsGroup = enabled) } } + .distinctUntilChanged() + .subscribe { enabled -> newState { copy(sendAsGroup = enabled) } } disposables += conversation - .map { conversation -> conversation.id } - .distinctUntilChanged() - .withLatestFrom(state) { id, state -> messageRepo.getMessages(id, state.query) } - .switchMap { messages -> messages.asObservable() } - .takeUntil(state.map { it.query }.filter { it.isEmpty() }) - .filter { messages -> messages.isLoaded } - .filter { messages -> messages.isValid } - .subscribe(searchResults::onNext) + .map { conversation -> conversation.id } + .distinctUntilChanged() + .withLatestFrom(state) { id, state -> messageRepo.getMessages(id, state.query) } + .switchMap { messages -> messages.asObservable() } + .takeUntil(state.map { it.query }.filter { it.isEmpty() }) + .filter { messages -> messages.isLoaded } + .filter { messages -> messages.isValid } + .subscribe(searchResults::onNext) // on conversation change/init, work out how many non-me participants of the conversation // have a valid address (subscriber number) for replying/sending to @@ -248,22 +257,27 @@ class ComposeViewModel @Inject constructor( newState { copy(validRecipientNumbers = validRecipientNumbers) } } - disposables += Observables.combineLatest(searchSelection, searchResults) { selected, messages -> + disposables += Observables.combineLatest, Unit>( + searchSelection, + searchResults, + ) { selected, messages -> if (selected == -1L) { messages.lastOrNull()?.let { message -> searchSelection.onNext(message.id) } } else { val position = messages.indexOfFirst { it.id == selected } + 1 newState { copy(searchSelectionPosition = position, searchResults = messages.size) } } + Unit }.subscribe() val latestSubId = messages - .map { messages -> messages.lastOrNull()?.subId ?: -1 } - .distinctUntilChanged() + .map { messages -> messages.lastOrNull()?.subId ?: -1 } + .distinctUntilChanged() val subscriptions = ActiveSubscriptionObservable(subscriptionManager) disposables += Observables.combineLatest(latestSubId, subscriptions) { subId, subs -> - val sub = if (subs.size > 1) subs.firstOrNull { it.subscriptionId == subId } ?: subs[0] else null + val sub = if (subs.size > 1) subs.firstOrNull { it.subscriptionId == subId } + ?: subs[0] else null newState { copy(subscription = sub) } }.subscribe() @@ -305,63 +319,69 @@ class ComposeViewModel @Inject constructor( } view.chipsSelectedIntent - .withLatestFrom(selectedChips) { hashmap, chips -> - // Filter out any numbers that are already selected - hashmap.filter { (address) -> - chips.none { recipient -> phoneNumberUtils.compare(address, recipient.address) } - } + .withLatestFrom(selectedChips) { hashmap, chips -> + // Filter out any numbers that are already selected + hashmap.filter { (address) -> + chips.none { recipient -> phoneNumberUtils.compare(address, recipient.address) } } - .filter { hashmap -> hashmap.isNotEmpty() } - .map { hashmap -> - hashmap.map { (address, lookupKey) -> - conversationRepo.getRecipients() - .asSequence() - .filter { recipient -> recipient.contact?.lookupKey == lookupKey } - .firstOrNull { recipient -> phoneNumberUtils.compare(recipient.address, address) } - ?: Recipient( - address = address, - contact = lookupKey?.let(contactRepo::getUnmanagedContact)) - } - } - .autoDisposable(view.scope()) - .subscribe { chips -> - chipsReducer.onNext { list -> list + chips } - view.showKeyboard() + } + .filter { hashmap -> hashmap.isNotEmpty() } + .map { hashmap -> + hashmap.map { (address, lookupKey) -> + conversationRepo.getRecipients() + .asSequence() + .filter { recipient -> recipient.contact?.lookupKey == lookupKey } + .firstOrNull { recipient -> + phoneNumberUtils.compare( + recipient.address, + address + ) + } + ?: Recipient( + address = address, + contact = lookupKey?.let(contactRepo::getUnmanagedContact) + ) } + } + .autoDispose(view.scope()) + .subscribe { chips -> + chipsReducer.onNext { list -> list + chips } + view.showKeyboard() + } // Set the contact suggestions list to visible when the add button is pressed view.optionsItemIntent - .filter { it == R.id.add } - .withLatestFrom(selectedChips) { _, chips -> - newState { copy(saveDraft = false) } // do not save draft on next activity invisibility - view.showContacts(sharing, chips) - } - .autoDisposable(view.scope()) - .subscribe() + .filter { it == R.id.add } + .withLatestFrom(selectedChips) { _, chips -> + newState { copy(saveDraft = false) } // do not save draft on next activity invisibility + view.showContacts(sharing, chips) + } + .autoDispose(view.scope()) + .subscribe() // Update the list of selected contacts when a new contact is selected or an existing one is deselected view.chipDeletedIntent - .autoDisposable(view.scope()) - .subscribe { contact -> - chipsReducer.onNext { contacts -> - val result = contacts.filterNot { it == contact } - if (result.isEmpty()) { - view.showContacts(sharing, result) - } - result + .autoDispose(view.scope()) + .subscribe { contact -> + chipsReducer.onNext { contacts -> + val result = contacts.filterNot { it == contact } + if (result.isEmpty()) { + view.showContacts(sharing, result) } + result } + } // When the menu is loaded, trigger a new state so that the menu options can be rendered correctly view.menuReadyIntent - .autoDisposable(view.scope()) - .subscribe { newState { copy() } } + .autoDispose(view.scope()) + .subscribe { newState { copy() } } // Show scheduled messages view.optionsItemIntent - .filter {it == R.id.viewScheduledMessages} + .filter { it == R.id.viewScheduledMessages } .withLatestFrom(state, conversation) - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe { (_, _, conversation) -> navigator.showScheduled(conversation.id) } @@ -369,7 +389,7 @@ class ComposeViewModel @Inject constructor( // toggle select all / select none view.optionsItemIntent .filter { it == R.id.select_all } - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe { view.toggleSelectAll() } // Open the phone dialer if the call button is clicked @@ -380,30 +400,30 @@ class ComposeViewModel @Inject constructor( state.messages?.second?.lastOrNull { !it.isMe() }?.address // most recent non-me msg address ?: conversation.recipients.firstOrNull()?.address // first recipient in convo } - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe { navigator.makePhoneCall(it) } // Open the conversation settings if info button is clicked view.optionsItemIntent - .filter { it == R.id.info } - .withLatestFrom(conversation) { _, conversation -> conversation } - .autoDisposable(view.scope()) - .subscribe { conversation -> navigator.showConversationInfo(conversation.id) } + .filter { it == R.id.info } + .withLatestFrom(conversation) { _, conversation -> conversation } + .autoDispose(view.scope()) + .subscribe { conversation -> navigator.showConversationInfo(conversation.id) } // Copy the message contents view.optionsItemIntent - .filter { it == R.id.copy } - .withLatestFrom(view.messagesSelectedIntent) { _, messageIds -> - ClipboardUtils.copy( - context, - messageIds - .mapNotNull(messageRepo::getMessage) - .sortedBy { it.date } - .getText() - ) - } - .autoDisposable(view.scope()) - .subscribe { view.clearSelection() } + .filter { it == R.id.copy } + .withLatestFrom(view.messagesSelectedIntent) { _, messageIds -> + ClipboardUtils.copy( + context, + messageIds + .mapNotNull(messageRepo::getMessage) + .sortedBy { it.date } + .getText() + ) + } + .autoDispose(view.scope()) + .subscribe { view.clearSelection() } // share the message text contents view.optionsItemIntent @@ -415,22 +435,23 @@ class ComposeViewModel @Inject constructor( SimpleDateFormat( "yyyy-MM-dd-HH-mm-ss", Locale.getDefault() - ).format(System.currentTimeMillis())}.txt" + ).format(System.currentTimeMillis()) + }.txt" val mimeType = "${MimeTypes.BASE_TYPE_TEXT}/plain" // save all messages text to a file in cache val (uri, e) = FileUtils.createAndWrite( - context, - FileUtils.Companion.Location.Cache, - filename, - mimeType, - messageIds - .mapNotNull(messageRepo::getMessage) - .sortedBy { it.date } - .getText() - .toByteArray() - ) + context, + FileUtils.Companion.Location.Cache, + filename, + mimeType, + messageIds + .mapNotNull(messageRepo::getMessage) + .sortedBy { it.date } + .getText() + .toByteArray() + ) if (e is Exception) Pair(filename, e) @@ -461,25 +482,25 @@ class ComposeViewModel @Inject constructor( else Timber.d("Created and shared messages text file: $filename", e) } - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe { view.clearSelection() } // Show the message details view.optionsItemIntent - .filter { it == R.id.details } - .withLatestFrom(view.messagesSelectedIntent) { _, messages -> messages } - .mapNotNull { messages -> messages.firstOrNull().also { view.clearSelection() } } - .mapNotNull(messageRepo::getMessage) - .map(messageDetailsFormatter::format) - .autoDisposable(view.scope()) - .subscribe { view.showDetails(it) } + .filter { it == R.id.details } + .withLatestFrom(view.messagesSelectedIntent) { _, messages -> messages } + .mapNotNull { messages -> messages.firstOrNull().also { view.clearSelection() } } + .mapNotNull(messageRepo::getMessage) + .map(messageDetailsFormatter::format) + .autoDispose(view.scope()) + .subscribe { view.showDetails(it) } // Show the delete message dialog if one or more messages selected view.optionsItemIntent .filter { it == R.id.delete } .withLatestFrom(view.messagesSelectedIntent) { _, selectedMessages -> selectedMessages } .filter { permissionManager.isDefaultSms().also { if (!it) view.requestDefaultSms() } } - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe { view.showDeleteDialog(it) } // show the clear current message dialog if no messages selected @@ -487,28 +508,36 @@ class ComposeViewModel @Inject constructor( .filter { it == R.id.delete } .withLatestFrom(state) { _, state -> state } .filter { it.selectedMessages == 0 } - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe { view.showClearCurrentMessageDialog() } // Forward the message view.optionsItemIntent .filter { it == R.id.forward } - .withLatestFrom(view.messagesSelectedIntent) { _, messages -> - messages?.firstOrNull()?.let { messageRepo.getMessage(it) }?.let { message -> - navigator.showCompose( - message.getText(), - message.parts.filter { !it.isSmil() }.mapNotNull { it.getUri() } - ) + .withLatestFrom(view.messagesSelectedIntent, + BiFunction, Pair>> { action, messages -> + action to messages + } + ) + .autoDispose(view.scope()) + .subscribe { pair -> + val messages = pair?.second + messages?.firstOrNull()?.let { id -> + messageRepo.getMessage(id)?.let { message -> + navigator.showCompose( + message.getText(), + message.parts.filter { !it.isSmil() }.mapNotNull { it.getUri() } + ) + view.clearSelection() + } } } - .autoDisposable(view.scope()) - .subscribe { view.clearSelection() } // expand message to show additional info view.optionsItemIntent .filter { it == R.id.show_status } .withLatestFrom(view.messagesSelectedIntent) { _, messages -> messages } - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe { messageIds -> view.expandMessages(messageIds, true) view.clearSelection() @@ -516,41 +545,44 @@ class ComposeViewModel @Inject constructor( // Show the previous search result view.optionsItemIntent - .filter { it == R.id.previous } - .withLatestFrom(searchSelection, searchResults) { _, selection, messages -> - val currentPosition = messages.indexOfFirst { it.id == selection } - if (currentPosition <= 0L) messages.lastOrNull()?.id ?: -1 - else messages.getOrNull(currentPosition - 1)?.id ?: -1 - } - .filter { id -> id != -1L } - .autoDisposable(view.scope()) - .subscribe(searchSelection) + .filter { it == R.id.previous } + .withLatestFrom(searchSelection, searchResults) { _, selection, messages -> + val currentPosition = messages.indexOfFirst { it.id == selection } + if (currentPosition <= 0L) messages.lastOrNull()?.id ?: -1 + else messages.getOrNull(currentPosition - 1)?.id ?: -1 + } + .filter { id -> id != -1L } + .autoDispose(view.scope()) + .subscribe(searchSelection) // Show the next search result view.optionsItemIntent - .filter { it == R.id.next } - .withLatestFrom(searchSelection, searchResults) { _, selection, messages -> - val currentPosition = messages.indexOfFirst { it.id == selection } - if (currentPosition >= messages.size - 1) messages.firstOrNull()?.id ?: -1 - else messages.getOrNull(currentPosition + 1)?.id ?: -1 - } - .filter { id -> id != -1L } - .autoDisposable(view.scope()) - .subscribe(searchSelection) + .filter { it == R.id.next } + .withLatestFrom(searchSelection, searchResults) { _, selection, messages -> + val currentPosition = messages.indexOfFirst { it.id == selection } + if (currentPosition >= messages.size - 1) messages.firstOrNull()?.id ?: -1 + else messages.getOrNull(currentPosition + 1)?.id ?: -1 + } + .filter { id -> id != -1L } + .autoDispose(view.scope()) + .subscribe(searchSelection) // Clear the search view.optionsItemIntent - .filter { it == R.id.clear } - .autoDisposable(view.scope()) - .subscribe { newState { copy(query = "", searchSelectionId = -1) } } + .filter { it == R.id.clear } + .autoDispose(view.scope()) + .subscribe { newState { copy(query = "", searchSelectionId = -1) } } // message part context menu item selected - save view.contextItemIntent .filter { it.itemId == R.id.save } - .filter { permissionManager.hasStorage().also { if (!it) view.requestStoragePermission() } } - .autoDisposable(view.scope()) + .filter { + permissionManager.hasStorage().also { if (!it) view.requestStoragePermission() } + } + .autoDispose(view.scope()) .subscribe { - val menuInfo = it.menuInfo as QkContextMenuRecyclerView.ContextMenuInfo + val menuInfo = + it.menuInfo as QkContextMenuRecyclerView.ContextMenuInfo if (menuInfo.viewHolderValue != null) saveImage.execute(menuInfo.viewHolderValue.id) { context.makeToast(R.string.gallery_toast_saved) @@ -560,9 +592,10 @@ class ComposeViewModel @Inject constructor( // message part context menu item selected - share view.contextItemIntent .filter { it.itemId == R.id.share } - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe { - val menuInfo = it.menuInfo as QkContextMenuRecyclerView.ContextMenuInfo + val menuInfo = + it.menuInfo as QkContextMenuRecyclerView.ContextMenuInfo if (menuInfo.viewHolderValue != null) navigator.shareFile( MmsPartProvider.getUriForMmsPartId( @@ -576,9 +609,10 @@ class ComposeViewModel @Inject constructor( // message part context menu item selected - forward view.contextItemIntent .filter { it.itemId == R.id.forward } - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe { - val menuInfo = it.menuInfo as QkContextMenuRecyclerView.ContextMenuInfo + val menuInfo = + it.menuInfo as QkContextMenuRecyclerView.ContextMenuInfo if (menuInfo.viewHolderValue != null) navigator.showCompose("", listOf(menuInfo.viewHolderValue.getUri())) } @@ -586,9 +620,10 @@ class ComposeViewModel @Inject constructor( // message part context menu item selected - open externally view.contextItemIntent .filter { it.itemId == R.id.openExternally } - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe { - val menuInfo = it.menuInfo as QkContextMenuRecyclerView.ContextMenuInfo + val menuInfo = + it.menuInfo as QkContextMenuRecyclerView.ContextMenuInfo if (menuInfo.viewHolderValue != null) navigator.viewFile( MmsPartProvider.getUriForMmsPartId( @@ -601,78 +636,81 @@ class ComposeViewModel @Inject constructor( // Toggle the group sending mode view.sendAsGroupIntent - .autoDisposable(view.scope()) - .subscribe { prefs.sendAsGroup.set(!prefs.sendAsGroup.get()) } + .autoDispose(view.scope()) + .subscribe { prefs.sendAsGroup.set(!prefs.sendAsGroup.get()) } // Scroll to search position searchSelection - .filter { id -> id != -1L } - .doOnNext { id -> newState { copy(searchSelectionId = id) } } - .autoDisposable(view.scope()) - .subscribe(view::scrollToMessage) + .filter { id -> id != -1L } + .doOnNext { id -> newState { copy(searchSelectionId = id) } } + .autoDispose(view.scope()) + .subscribe(view::scrollToMessage) // Theme changes prefs.keyChanges - .filter { key -> key.contains("theme") } - .doOnNext { view.themeChanged() } - .autoDisposable(view.scope()) - .subscribe() + .filter { key -> key.contains("theme") } + .doOnNext { view.themeChanged() } + .autoDispose(view.scope()) + .subscribe() // Media attachment clicks view.messagePartClickIntent - .mapNotNull(messageRepo::getPart) - .filter { part -> part.isImage() || part.isVideo() } - .autoDisposable(view.scope()) - .subscribe { part -> navigator.showMedia(part.id) } + .mapNotNull(messageRepo::getPart) + .filter { part -> part.isImage() || part.isVideo() } + .autoDispose(view.scope()) + .subscribe { part -> navigator.showMedia(part.id) } // Non-media attachment clicks view.messagePartClickIntent - .mapNotNull(messageRepo::getPart) - .filter { part -> !part.isImage() && !part.isVideo() } - .autoDisposable(view.scope()) - .subscribe { - navigator.viewFile( - MmsPartProvider.getUriForMmsPartId(it.id, it.getBestFilename()), - it.type - ) - } + .mapNotNull(messageRepo::getPart) + .filter { part -> !part.isImage() && !part.isVideo() } + .autoDispose(view.scope()) + .subscribe { + navigator.viewFile( + MmsPartProvider.getUriForMmsPartId(it.id, it.getBestFilename()), + it.type + ) + } // Update the State when the message selected count changes view.messagesSelectedIntent - .map { - Pair( - it.size, - it.any { messageRepo.getMessage(it)?.hasNonWhitespaceText() ?: false } + .map { + Pair( + it.size, + it.any { messageRepo.getMessage(it)?.hasNonWhitespaceText() ?: false } + ) + } + .autoDispose(view.scope()) + .subscribe { + newState { + copy( + selectedMessages = it.first, + selectedMessagesHaveText = it.second, + editingMode = false ) } - .autoDisposable(view.scope()) - .subscribe { - newState { - copy( - selectedMessages = it.first, - selectedMessagesHaveText = it.second, - editingMode = false - ) - } - } + } // Cancel sending a message view.cancelSendingIntent - .mapNotNull(messageRepo::getMessage) - .doOnNext { message -> view.setDraft(message.getText(false)) } - .autoDisposable(view.scope()) - .subscribe { message -> - cancelMessage.execute(CancelDelayedMessage.Params(message.id, message.threadId)) - } + .mapNotNull(messageRepo::getMessage) + .doOnNext { message -> view.setDraft(message.getText(false)) } + .autoDispose(view.scope()) + .subscribe { message -> + cancelMessage.execute(CancelDelayedMessage.Params(message.id, message.threadId)) + } // send a delayed message now view.sendNowIntent .mapNotNull(messageRepo::getMessage) - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe { message -> cancelMessage.execute(CancelDelayedMessage.Params(message.id, message.threadId)) - val address = listOf(conversationRepo - .getConversation(threadId)?.recipients?.firstOrNull()?.address ?: message.address) + val address = listOf( + conversationRepo + .getConversation(threadId)?.recipients?.firstOrNull()?.address + ?: message.address + ) sendMessage.execute( SendMessage.Params( message.subId, @@ -690,164 +728,170 @@ class ComposeViewModel @Inject constructor( .mapNotNull(messageRepo::getMessage) .filter { message -> message.isFailedMessage() } .doOnNext { message -> retrySending.execute(message.id) } - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe() // Show the message details view.messageLinkAskIntent - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe { view.showMessageLinkAskDialog(it) } // Set the current conversation Observables - .combineLatest( - view.activityVisibleIntent.distinctUntilChanged(), - conversation.mapNotNull { conversation -> - conversation.takeIf { it.isValid }?.id - }.distinctUntilChanged()) - { visible, threadId -> - when (visible) { - true -> { - activeConversationManager.setActiveConversation(threadId) - markRead.execute(listOf(threadId)) - } - - false -> activeConversationManager.setActiveConversation(null) + .combineLatest( + view.activityVisibleIntent.distinctUntilChanged(), + conversation.mapNotNull { conversation -> + conversation.takeIf { it.isValid }?.id + }.distinctUntilChanged() + ) + { visible, threadId -> + when (visible) { + true -> { + activeConversationManager.setActiveConversation(threadId) + markRead.execute(listOf(threadId)) } + + false -> activeConversationManager.setActiveConversation(null) } - .autoDisposable(view.scope()) - .subscribe() + } + .autoDispose(view.scope()) + .subscribe() // Save draft when the activity goes into the background view.activityVisibleIntent - .filter { visible -> !visible } - .withLatestFrom(conversation) { _, conversation -> conversation } - .mapNotNull { conversation -> conversation.takeIf { it.isValid }?.id } - .observeOn(Schedulers.io()) - .withLatestFrom(view.textChangedIntent, state) { threadId, draftText, state -> - if (state.saveDraft) - conversationRepo.saveDraft( - threadId, - if (draftText.isNotBlank()) draftText.toString() - else "" - ) + .filter { visible -> !visible } + .withLatestFrom(conversation) { _, conversation -> conversation } + .mapNotNull { conversation -> conversation.takeIf { it.isValid }?.id } + .observeOn(Schedulers.io()) + .withLatestFrom(view.textChangedIntent, state) { threadId, draftText, state -> + if (state.saveDraft) + conversationRepo.saveDraft( + threadId, + if (draftText.isNotBlank()) draftText.toString() + else "" + ) - // remove attachments - state.attachments.forEach { it.removeCacheFile() } + // remove attachments + state.attachments.forEach { it.removeCacheFile() } - newState { copy(saveDraft = true) } - } - .autoDisposable(view.scope()) - .subscribe() + newState { copy(saveDraft = true) } + } + .autoDispose(view.scope()) + .subscribe() // Open the attachment options view.attachIntent - .autoDisposable(view.scope()) - .subscribe { newState { copy(attaching = !attaching) } } + .autoDispose(view.scope()) + .subscribe { newState { copy(attaching = !attaching) } } // Attach a photo from camera view.cameraIntent - .autoDisposable(view.scope()) - .subscribe { - newState { copy(attaching = false) } - view.requestCamera() - } + .autoDispose(view.scope()) + .subscribe { + newState { copy(attaching = false) } + view.requestCamera() + } // pick a photo (specifically) from image provider apps view.attachImageFileIntent .doOnNext { newState { copy(attaching = false) } } - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe { view.requestGallery("image/*", ComposeView.AttachAFileRequestCode) } // pick any file from any provider apps view.attachAnyFileIntent .doOnNext { newState { copy(attaching = false) } } - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe { view.requestGallery("*/*", ComposeView.AttachAFileRequestCode) } // Choose a time to schedule the message view.scheduleIntent - .doOnNext { newState { copy(attaching = false) } } - .withLatestFrom(billingManager.upgradeStatus) { _, upgraded -> upgraded } - .filter { upgraded -> - upgraded.also { if (!upgraded) view.showQksmsPlusSnackbar(R.string.compose_scheduled_plus) } - } - .autoDisposable(view.scope()) - .subscribe { view.requestDatePicker() } + .doOnNext { newState { copy(attaching = false) } } + .withLatestFrom(billingManager.upgradeStatus) { _, upgraded -> upgraded } + .filter { upgraded -> + upgraded.also { if (!upgraded) view.showQksmsPlusSnackbar(R.string.compose_scheduled_plus) } + } + .autoDispose(view.scope()) + .subscribe { view.requestDatePicker() } view.scheduleAction .take(1) - .doOnNext{ newState { copy(scheduling = false) } } - .autoDisposable(view.scope()) + .doOnNext { newState { copy(scheduling = false) } } + .autoDispose(view.scope()) .subscribe { view.requestDatePicker() } // an attachment was picked by the user Observable.merge( view.attachAnyFileSelectedIntent.map { uri -> Attachment(context, uri) }, - view.inputContentIntent.map { inputContent -> Attachment(context, inputContent = inputContent) } + view.inputContentIntent.map { inputContent -> + Attachment( + context, + inputContent = inputContent + ) + } ) - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe { newState { copy(attachments = attachments + it, attaching = false) } } // Set the scheduled time view.scheduleSelectedIntent - .filter { scheduled -> - (scheduled > System.currentTimeMillis()).also { future -> - if (!future) context.makeToast(R.string.compose_scheduled_future) - } + .filter { scheduled -> + (scheduled > System.currentTimeMillis()).also { future -> + if (!future) context.makeToast(R.string.compose_scheduled_future) } - .autoDisposable(view.scope()) - .subscribe { scheduled -> newState { copy(scheduled = scheduled) } } + } + .autoDispose(view.scope()) + .subscribe { scheduled -> newState { copy(scheduled = scheduled) } } // Attach a contact view.attachContactIntent - .doOnNext { newState { copy(attaching = false) } } - .autoDisposable(view.scope()) - .subscribe { view.requestContact() } + .doOnNext { newState { copy(attaching = false) } } + .autoDispose(view.scope()) + .subscribe { view.requestContact() } // Contact was selected for attachment view.contactSelectedIntent - .subscribeOn(Schedulers.io()) - .autoDisposable(view.scope()) - .subscribe( - { - newState { - copy(attachments = attachments + Attachment(context, uri = it)) - } + .subscribeOn(Schedulers.io()) + .autoDispose(view.scope()) + .subscribe( + { + newState { + copy(attachments = attachments + Attachment(context, uri = it)) } - ) { error -> - context.makeToast(R.string.compose_contact_error) - Timber.w(error) } + ) { error -> + context.makeToast(R.string.compose_contact_error) + Timber.w(error) + } // Detach an attachment view.attachmentDeletedIntent - .autoDisposable(view.scope()) - .subscribe { - newState { copy(attachments = attachments - it) } + .autoDispose(view.scope()) + .subscribe { + newState { copy(attachments = attachments - it) } - // if the attachment is backed by a local file, delete the file - it.removeCacheFile() - } + // if the attachment is backed by a local file, delete the file + it.removeCacheFile() + } conversation - .map { conversation -> conversation.draft } - .distinctUntilChanged() - .autoDisposable(view.scope()) - .subscribe { draft -> - - // If text was shared into the conversation, it should take priority over the - // existing draft - // - // TODO: Show dialog warning user about overwriting draft - if (sharedText.isNotBlank()) { - view.setDraft(sharedText) - } else { - view.setDraft(draft) - } + .map { conversation -> conversation.draft } + .distinctUntilChanged() + .autoDispose(view.scope()) + .subscribe { draft -> + + // If text was shared into the conversation, it should take priority over the + // existing draft + // + // TODO: Show dialog warning user about overwriting draft + if (sharedText.isNotBlank()) { + view.setDraft(sharedText) + } else { + view.setDraft(draft) } + } // set canSend state depending on if there is text input, an attachment or a schedule set Observables.combineLatest( @@ -858,7 +902,7 @@ class ComposeViewModel @Inject constructor( state.distinctUntilChanged { state -> state.scheduled } // schedule set or not .map { it.scheduled } ) - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe { newState { copy( @@ -870,59 +914,71 @@ class ComposeViewModel @Inject constructor( // Show the remaining character counter when necessary view.textChangedIntent - .observeOn(Schedulers.computation()) - .mapNotNull { draft -> tryOrNull { SmsMessage.calculateLength(draft, prefs.unicode.get()) } } - .map { array -> - val messages = array[0] - val remaining = array[2] - - when { - messages <= 1 && remaining > 10 -> "" - messages <= 1 && remaining <= 10 -> "$remaining" - else -> "$remaining / $messages" - } + .observeOn(Schedulers.computation()) + .mapNotNull { draft -> + tryOrNull { + SmsMessage.calculateLength( + draft, + prefs.unicode.get() + ) + } + } + .map { array -> + val messages = array[0] + val remaining = array[2] + + when { + messages <= 1 && remaining > 10 -> "" + messages <= 1 && remaining <= 10 -> "$remaining" + else -> "$remaining / $messages" } - .distinctUntilChanged() - .autoDisposable(view.scope()) - .subscribe { remaining -> newState { copy(remaining = remaining) } } + } + .distinctUntilChanged() + .autoDispose(view.scope()) + .subscribe { remaining -> newState { copy(remaining = remaining) } } // Cancel the scheduled time view.scheduleCancelIntent - .autoDisposable(view.scope()) - .subscribe { newState { copy(scheduled = 0) } } + .autoDispose(view.scope()) + .subscribe { newState { copy(scheduled = 0) } } // Toggle to the next sim slot view.changeSimIntent - .withLatestFrom(state) { _, state -> - val subs = subscriptionManager.activeSubscriptionInfoList - val subIndex = subs.indexOfFirst { it.subscriptionId == state.subscription?.subscriptionId } - val subscription = when { - subIndex == -1 -> null - subIndex < subs.size - 1 -> subs[subIndex + 1] - else -> subs[0] - } - - if (subscription != null) { - context.getSystemService()?.vibrate(40) - context.makeToast(context.getString(R.string.compose_sim_changed_toast, - subscription.simSlotIndex + 1, subscription.displayName)) - } + .withLatestFrom(state) { _, state -> + val subs = subscriptionManager.activeSubscriptionInfoList + val subIndex = + subs.indexOfFirst { it.subscriptionId == state.subscription?.subscriptionId } + val subscription = when { + subIndex == -1 -> null + subIndex < subs.size - 1 -> subs[subIndex + 1] + else -> subs[0] + } - newState { copy(subscription = subscription) } + if (subscription != null) { + context.getSystemService()?.vibrate(40) + context.makeToast( + context.getString( + R.string.compose_sim_changed_toast, + subscription.simSlotIndex + 1, subscription.displayName + ) + ) } - .autoDisposable(view.scope()) - .subscribe() + + newState { copy(subscription = subscription) } + } + .autoDispose(view.scope()) + .subscribe() // shade clicked view.shadeIntent - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe { newState { copy(attaching = false) } } // starting or stopping (change state) of audio message ui state .distinctUntilChanged { state -> state.audioMsgRecording } .skip(1) // skip initial value - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe { // stop any audio playback (ie from mms attachment or audio recorder) QkMediaPlayer.reset() @@ -937,7 +993,7 @@ class ComposeViewModel @Inject constructor( // starting or stopping the recording of audio view.recordAudioStartStopRecording - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe { // if start recording if (it == true) { @@ -945,8 +1001,8 @@ class ComposeViewModel @Inject constructor( // check have permissions to record audio if (permissionManager.hasRecordAudio().also { - if (!it) view.requestRecordAudioPermission() - }) { + if (!it) view.requestRecordAudioPermission() + }) { // create bluetooth mic device manager bluetoothMicManager?.close() bluetoothMicManager = BluetoothMicManager( @@ -956,17 +1012,22 @@ class ComposeViewModel @Inject constructor( // no bluetooth sco device found, use built-in mic this.onConnected(null) } + override fun onDeviceFound(device: AudioDeviceInfo?) { // show bluetooth placeholder until bluetooth connected view.recordAudioMsgRecordVisible.onNext(false) } - override fun onConnecting(device: AudioDeviceInfo?) { /* nothing */ } + + override fun onConnecting(device: AudioDeviceInfo?) { /* nothing */ + } + override fun onConnected(device: AudioDeviceInfo?) { // show record button and chronometer, hide bluetooth placeholder view.recordAudioMsgRecordVisible.onNext(true) view.recordAudioChronometer.onNext(true) // start chronometer MediaRecorderManager.startRecording(context, device) } + override fun onDisconnected(device: AudioDeviceInfo?) { // if bluetooth disconnects, stop recording if (device != null) { @@ -989,21 +1050,21 @@ class ComposeViewModel @Inject constructor( // record an audio message menu item or main mic icon view.recordAnAudioMessage - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe { view.recordAudioStartStopRecording.onNext(true) // start recording - newState { copy( attaching = false, audioMsgRecording = true) } + newState { copy(attaching = false, audioMsgRecording = true) } } // abort recording audio message button view.recordAudioAbort .observeOn(Schedulers.io()) - .autoDisposable(view.scope()) - .subscribe { newState { copy( audioMsgRecording = false) } } + .autoDispose(view.scope()) + .subscribe { newState { copy(audioMsgRecording = false) } } // main record/stop recording audio message button view.recordAudioRecord - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe { if (it == MicInputCloudView.ViewState.PAUSED_STATE) { view.recordAudioStartStopRecording.onNext(false) // stop recording @@ -1016,7 +1077,7 @@ class ComposeViewModel @Inject constructor( // attach recorded audio message button view.recordAudioAttach - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe { MediaRecorderManager.stopRecording() @@ -1042,23 +1103,25 @@ class ComposeViewModel @Inject constructor( attachments = attachments + Attachment(context, newUri) ) } + } catch (e: Exception) { /* nothing */ } - catch (e: Exception) { /* nothing */ } } // audio recording player play/pause button view.recordAudioPlayerPlayPause - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe { when (it) { QkMediaPlayer.PlayingState.Paused -> view.recordAudioPlayerConfigUI.onNext( QkMediaPlayer.PlayingState.Playing ) + QkMediaPlayer.PlayingState.Playing -> view.recordAudioPlayerConfigUI.onNext( QkMediaPlayer.PlayingState.Paused ) + else -> { if (MediaRecorderManager.uri != Uri.EMPTY) { QkMediaPlayer.setOnPreparedListener { @@ -1129,7 +1192,7 @@ class ComposeViewModel @Inject constructor( } val sendAsGroup = ((addresses.size > 1) && // if more than one address to send to (!state.editingMode || // and is not a new convo (group msg or not is already set) - state.sendAsGroup)) // or (is a new convo and) send as group is selected + state.sendAsGroup)) // or (is a new convo and) send as group is selected when { // Scheduling a message @@ -1144,7 +1207,7 @@ class ComposeViewModel @Inject constructor( conversationId ) ).also { - newState { copy(scheduled = 0, hasScheduledMessages = true ) } + newState { copy(scheduled = 0, hasScheduledMessages = true) } showScheduledToast = true } @@ -1191,35 +1254,38 @@ class ComposeViewModel @Inject constructor( showScheduledToast = false } } - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe() // View QKSMS+ view.viewQksmsPlusIntent - .autoDisposable(view.scope()) - .subscribe { navigator.showQksmsPlusActivity("compose_schedule") } + .autoDispose(view.scope()) + .subscribe { navigator.showQksmsPlusActivity("compose_schedule") } // Navigate back view.optionsItemIntent - .filter { it == android.R.id.home } - .map { Unit } - .mergeWith(view.backPressedIntent) - .withLatestFrom(state) { _, state -> - when { - state.selectedMessages > 0 -> view.clearSelection() - else -> newState { copy(hasError = true) } - } + .filter { it == android.R.id.home } + .map { } + .mergeWith(view.backPressedIntent) + .withLatestFrom(state) { _, state -> + when { + state.selectedMessages > 0 -> view.clearSelection() + else -> newState { copy(hasError = true) } } - .autoDisposable(view.scope()) - .subscribe() + } + .autoDispose(view.scope()) + .subscribe() // Delete the message view.confirmDeleteIntent - .withLatestFrom(view.messagesSelectedIntent, conversation) { _, messages, conversation -> - deleteMessages.execute(DeleteMessages.Params(messages.toList(), conversation.id)) - } - .autoDisposable(view.scope()) - .subscribe { view.clearSelection() } + .withLatestFrom( + view.messagesSelectedIntent, + conversation + ) { _, messages, conversation -> + deleteMessages.execute(DeleteMessages.Params(messages.toList(), conversation.id)) + } + .autoDispose(view.scope()) + .subscribe { view.clearSelection() } // clear the current message schedule, text and attachments view.clearCurrentMessageIntent @@ -1229,7 +1295,7 @@ class ComposeViewModel @Inject constructor( state.attachments.forEach { it.removeCacheFile() } hasError } - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe { view.setDraft("") newState { diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/MessagesAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/MessagesAdapter.kt index 4e0dc8ad7..2ab38b95d 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/MessagesAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/MessagesAdapter.kt @@ -36,10 +36,13 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView -import android.widget.ProgressBar import androidx.core.net.toUri import com.jakewharton.rxbinding2.view.clicks import com.moez.QKSMS.common.QkMediaPlayer +import io.reactivex.disposables.Disposable +import io.reactivex.subjects.PublishSubject +import io.reactivex.subjects.Subject +import io.realm.RealmResults import org.prauga.messages.R import org.prauga.messages.common.Navigator import org.prauga.messages.common.base.QkRealmAdapter @@ -54,6 +57,8 @@ import org.prauga.messages.common.util.extensions.setTint import org.prauga.messages.common.util.extensions.setVisible import org.prauga.messages.common.util.extensions.withAlpha import org.prauga.messages.compat.SubscriptionManagerCompat +import org.prauga.messages.databinding.MessageListItemInBinding +import org.prauga.messages.databinding.MessageListItemOutBinding import org.prauga.messages.extensions.isSmil import org.prauga.messages.extensions.isText import org.prauga.messages.extensions.joinTo @@ -68,13 +73,6 @@ import org.prauga.messages.model.Message import org.prauga.messages.model.Recipient import org.prauga.messages.util.PhoneNumberUtils import org.prauga.messages.util.Preferences -import org.prauga.messages.databinding.MessageListItemInBinding -import org.prauga.messages.databinding.MessageListItemOutBinding -import io.reactivex.disposables.Disposable -import io.reactivex.subjects.PublishSubject -import io.reactivex.subjects.Subject -import io.realm.RealmResults -import java.util.* import java.util.concurrent.TimeUnit import javax.inject.Inject import javax.inject.Provider @@ -117,7 +115,8 @@ class MessagesAdapter @Inject constructor( } // Wrapper for MessageListItemOutBinding - private class OutBindingWrapper(private val binding: MessageListItemOutBinding) : MessageBinding { + private class OutBindingWrapper(private val binding: MessageListItemOutBinding) : + MessageBinding { override val timestamp = binding.timestamp override val sim = binding.sim override val simIndex = binding.simIndex @@ -340,10 +339,10 @@ class MessagesAdapter @Inject constructor( binding.timestamp.apply { text = dateFormatter.getMessageTimestamp(message.date) setVisible( - ((message.date - (previous?.date ?: 0)) - .millisecondsToMinutes() >= BubbleUtils.TIMESTAMP_THRESHOLD) || - (message.subId != previous?.subId) && - (subscription != null) + ((message.date - (previous?.date ?: 0)) + .millisecondsToMinutes() >= BubbleUtils.TIMESTAMP_THRESHOLD) || + (message.subId != previous?.subId) && + (subscription != null) ) } @@ -430,6 +429,7 @@ class MessagesAdapter @Inject constructor( } } } + else -> binding.body.movementMethod = LinkMovementMethod.getInstance() } @@ -513,12 +513,15 @@ class MessagesAdapter @Inject constructor( R.string.message_status_delivered, dateFormatter.getTimestamp(message.dateSent) ) + message.isFailedMessage() -> context.getString(R.string.message_status_failed) bodyTextTruncated -> context.getString(R.string.message_body_too_long_to_display) (!message.isMe() && (conversation?.recipients?.size ?: 0) > 1) -> // incoming group message "${contactCache[message.address]?.getDisplayName()} • ${ - dateFormatter.getTimestamp(message.date)}" + dateFormatter.getTimestamp(message.date) + }" + else -> dateFormatter.getTimestamp(message.date) } @@ -535,6 +538,7 @@ class MessagesAdapter @Inject constructor( expanded[message.id] == false -> false ((conversation?.recipients?.size ?: 0) > 1) && !message.isMe() && next?.compareSender(message) != true -> true + (message.isDelivered() && (next?.isDelivered() != true) && (age <= BubbleUtils.TIMESTAMP_THRESHOLD)) -> true diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ChipsAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ChipsAdapter.kt index 15130075b..d4035a8fc 100755 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ChipsAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ChipsAdapter.kt @@ -23,20 +23,24 @@ import android.view.LayoutInflater import android.view.ViewGroup import android.widget.RelativeLayout import androidx.recyclerview.widget.RecyclerView +import io.reactivex.subjects.PublishSubject import org.prauga.messages.common.base.QkAdapter import org.prauga.messages.common.base.QkBindingViewHolder import org.prauga.messages.common.util.extensions.dpToPx import org.prauga.messages.databinding.ContactChipBinding import org.prauga.messages.model.Recipient -import io.reactivex.subjects.PublishSubject import javax.inject.Inject -class ChipsAdapter @Inject constructor() : QkAdapter>() { +class ChipsAdapter @Inject constructor() : + QkAdapter>() { var view: RecyclerView? = null val chipDeleted: PublishSubject = PublishSubject.create() - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QkBindingViewHolder { + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): QkBindingViewHolder { val binding = ContactChipBinding.inflate(LayoutInflater.from(parent.context), parent, false) return QkBindingViewHolder(binding).apply { binding.root.setOnClickListener { @@ -50,7 +54,8 @@ class ChipsAdapter @Inject constructor() : QkAdapter = value.recipients.map { recipient -> - recipient.contact ?: Contact(numbers = RealmList(PhoneNumber(address = recipient.address))) + recipient.contact + ?: Contact(numbers = RealmList(PhoneNumber(address = recipient.address))) } } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ComposeItemAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ComposeItemAdapter.kt index c617848ec..043794030 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ComposeItemAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/ComposeItemAdapter.kt @@ -22,6 +22,10 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.rxkotlin.plusAssign +import io.reactivex.subjects.PublishSubject +import io.reactivex.subjects.Subject import org.prauga.messages.R import org.prauga.messages.common.base.QkAdapter import org.prauga.messages.common.base.QkViewHolder @@ -35,10 +39,6 @@ import org.prauga.messages.model.ContactGroup import org.prauga.messages.model.Conversation import org.prauga.messages.model.Recipient import org.prauga.messages.repository.ConversationRepository -import io.reactivex.disposables.CompositeDisposable -import io.reactivex.rxkotlin.plusAssign -import io.reactivex.subjects.PublishSubject -import io.reactivex.subjects.Subject import javax.inject.Inject class ComposeItemAdapter @Inject constructor( @@ -130,8 +130,8 @@ class ComposeItemAdapter @Inject constructor( binding.numbers.isVisible = conversation.recipients.size == 1 (binding.numbers.adapter as PhoneNumberAdapter).data = conversation.recipients - .mapNotNull { recipient -> recipient.contact } - .flatMap { contact -> contact.numbers } + .mapNotNull { recipient -> recipient.contact } + .flatMap { contact -> contact.numbers } } private fun bindStarred(holder: QkViewHolder, contact: Contact, prev: ComposeItem?) { @@ -175,9 +175,13 @@ class ComposeItemAdapter @Inject constructor( val binding = ContactListItemBinding.bind(holder.containerView) binding.index.isVisible = true - binding.index.text = if (contact.name.getOrNull(0)?.isLetter() == true) contact.name[0].toString() else "#" + binding.index.text = + if (contact.name.getOrNull(0)?.isLetter() == true) contact.name[0].toString() else "#" binding.index.isVisible = prev !is ComposeItem.Person || - (contact.name[0].isLetter() && !contact.name[0].equals(prev.value.name[0], ignoreCase = true)) || + (contact.name[0].isLetter() && !contact.name[0].equals( + prev.value.name[0], + ignoreCase = true + )) || (!contact.name[0].isLetter() && prev.value.name[0].isLetter()) binding.icon.isVisible = false @@ -195,13 +199,14 @@ class ComposeItemAdapter @Inject constructor( private fun createRecipient(contact: Contact): Recipient { return recipients[contact.lookupKey] ?: Recipient( address = contact.numbers.firstOrNull()?.address ?: "", - contact = contact) + contact = contact + ) } override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { disposables += conversationRepo.getUnmanagedRecipients() - .map { recipients -> recipients.associateByNotNull { recipient -> recipient.contact?.lookupKey } } - .subscribe { recipients -> this@ComposeItemAdapter.recipients = recipients } + .map { recipients -> recipients.associateByNotNull { recipient -> recipient.contact?.lookupKey } } + .subscribe { recipients -> this@ComposeItemAdapter.recipients = recipients } } override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) { diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/DetailedChipView.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/DetailedChipView.kt index 283da75ea..3e2fe4294 100755 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/DetailedChipView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/DetailedChipView.kt @@ -32,12 +32,17 @@ import javax.inject.Inject class DetailedChipView(context: Context) : RelativeLayout(context) { - @Inject lateinit var colors: Colors + @Inject + lateinit var colors: Colors private val binding: ContactChipDetailedBinding init { - binding = ContactChipDetailedBinding.inflate(android.view.LayoutInflater.from(context), this, true) + binding = ContactChipDetailedBinding.inflate( + android.view.LayoutInflater.from(context), + this, + true + ) appComponent.inject(this) setOnClickListener { hide() } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/PhoneNumberAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/PhoneNumberAdapter.kt index 428d4b227..83cae1f17 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/PhoneNumberAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/PhoneNumberAdapter.kt @@ -20,20 +20,27 @@ package org.prauga.messages.feature.compose.editing import android.view.LayoutInflater import android.view.ViewGroup -import org.prauga.messages.R import org.prauga.messages.common.base.QkAdapter import org.prauga.messages.common.base.QkBindingViewHolder import org.prauga.messages.databinding.ContactNumberListItemBinding import org.prauga.messages.model.PhoneNumber -class PhoneNumberAdapter : QkAdapter>() { +class PhoneNumberAdapter : + QkAdapter>() { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QkBindingViewHolder { - val binding = ContactNumberListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): QkBindingViewHolder { + val binding = + ContactNumberListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) return QkBindingViewHolder(binding) } - override fun onBindViewHolder(holder: QkBindingViewHolder, position: Int) { + override fun onBindViewHolder( + holder: QkBindingViewHolder, + position: Int + ) { val number = getItem(position) holder.binding.address.text = number.address diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/PhoneNumberPickerAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/PhoneNumberPickerAdapter.kt index edb1a86b4..954a7fa28 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/PhoneNumberPickerAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/editing/PhoneNumberPickerAdapter.kt @@ -21,6 +21,8 @@ package org.prauga.messages.feature.compose.editing import android.content.Context import android.view.LayoutInflater import android.view.ViewGroup +import io.reactivex.subjects.BehaviorSubject +import io.reactivex.subjects.Subject import org.prauga.messages.R import org.prauga.messages.common.base.QkAdapter import org.prauga.messages.common.base.QkViewHolder @@ -28,8 +30,6 @@ import org.prauga.messages.common.util.extensions.forwardTouches import org.prauga.messages.databinding.PhoneNumberListItemBinding import org.prauga.messages.extensions.Optional import org.prauga.messages.model.PhoneNumber -import io.reactivex.subjects.BehaviorSubject -import io.reactivex.subjects.Subject import javax.inject.Inject class PhoneNumberPickerAdapter @Inject constructor( @@ -40,9 +40,11 @@ class PhoneNumberPickerAdapter @Inject constructor( private var selectedItem: Long? = null set(value) { - data.indexOfFirst { number -> number.id == field }.takeIf { it != -1 }?.run(::notifyItemChanged) + data.indexOfFirst { number -> number.id == field }.takeIf { it != -1 } + ?.run(::notifyItemChanged) field = value - data.indexOfFirst { number -> number.id == field }.takeIf { it != -1 }?.run(::notifyItemChanged) + data.indexOfFirst { number -> number.id == field }.takeIf { it != -1 } + ?.run(::notifyItemChanged) selectedItemChanges.onNext(Optional(value)) } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/AudioBinder.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/AudioBinder.kt index a0b6bfd8b..84b971d26 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/AudioBinder.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/AudioBinder.kt @@ -25,6 +25,9 @@ import android.media.MediaMetadataRetriever import android.view.View import android.widget.SeekBar import com.moez.QKSMS.common.QkMediaPlayer +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers import org.prauga.messages.R import org.prauga.messages.common.Navigator import org.prauga.messages.common.base.QkViewHolder @@ -41,9 +44,6 @@ import org.prauga.messages.feature.compose.MessagesAdapter import org.prauga.messages.model.Message import org.prauga.messages.model.MmsPart import org.prauga.messages.util.GlideApp -import io.reactivex.Observable -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.schedulers.Schedulers import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -55,7 +55,8 @@ class AudioBinder @Inject constructor(colors: Colors, private val context: Conte const val DEFAULT_SHARE_FILENAME = "quik-audio-attachment.mp3" } - @Inject lateinit var navigator: Navigator + @Inject + lateinit var navigator: Navigator override val partLayout = R.layout.mms_audio_preview_list_item override var theme = colors.theme() @@ -130,6 +131,7 @@ class AudioBinder @Inject constructor(colors: Colors, private val context: Conte audioState.seekBarUpdater?.dispose() } } + QkMediaPlayer.PlayingState.Paused -> { if (audioState.partId == part.id) { QkMediaPlayer.start() @@ -140,6 +142,7 @@ class AudioBinder @Inject constructor(colors: Colors, private val context: Conte startSeekBarUpdateTimer() } } + else -> { if (part.getUri().resourceExists(context)) { QkMediaPlayer.reset() // make sure reset before trying to (re-)use @@ -222,18 +225,24 @@ class AudioBinder @Inject constructor(colors: Colors, private val context: Conte if (fromUser) QkMediaPlayer.seekTo(progress) } - override fun onStartTrackingTouch(p0: SeekBar?) { /* nothing */ } - override fun onStopTrackingTouch(p0: SeekBar?) { /* nothing */ } + + override fun onStartTrackingTouch(p0: SeekBar?) { /* nothing */ + } + + override fun onStopTrackingTouch(p0: SeekBar?) { /* nothing */ + } }) } // playPause button - binding.playPause. apply { + binding.playPause.apply { if ((audioState.partId == part.id) && - (audioState.state == QkMediaPlayer.PlayingState.Playing)) + (audioState.state == QkMediaPlayer.PlayingState.Playing) + ) uiToPlaying(holder) else if ((audioState.partId == part.id) && - (audioState.state == QkMediaPlayer.PlayingState.Paused)) + (audioState.state == QkMediaPlayer.PlayingState.Paused) + ) uiToPaused(holder) else uiToStopped(holder) @@ -264,10 +273,13 @@ class AudioBinder @Inject constructor(colors: Colors, private val context: Conte bubbleStyle = when { !canGroupWithPrevious && canGroupWithNext -> if (message.isMe()) BubbleImageView.Style.OUT_FIRST else BubbleImageView.Style.IN_FIRST + canGroupWithPrevious && canGroupWithNext -> if (message.isMe()) BubbleImageView.Style.OUT_MIDDLE else BubbleImageView.Style.IN_MIDDLE + canGroupWithPrevious && !canGroupWithNext -> if (message.isMe()) BubbleImageView.Style.OUT_LAST else BubbleImageView.Style.IN_LAST + else -> BubbleImageView.Style.ONLY } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/FileBinder.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/FileBinder.kt index 3190989dc..667f207c9 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/FileBinder.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/FileBinder.kt @@ -20,6 +20,9 @@ package org.prauga.messages.feature.compose.part import android.annotation.SuppressLint import android.content.Context +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers import org.prauga.messages.R import org.prauga.messages.common.Navigator import org.prauga.messages.common.base.QkViewHolder @@ -31,14 +34,12 @@ import org.prauga.messages.databinding.MmsFileListItemBinding import org.prauga.messages.feature.compose.BubbleUtils import org.prauga.messages.model.Message import org.prauga.messages.model.MmsPart -import io.reactivex.Observable -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.schedulers.Schedulers import javax.inject.Inject class FileBinder @Inject constructor(colors: Colors, private val context: Context) : PartBinder() { - @Inject lateinit var navigator: Navigator + @Inject + lateinit var navigator: Navigator override val partLayout = R.layout.mms_file_list_item override var theme = colors.theme() @@ -57,23 +58,23 @@ class FileBinder @Inject constructor(colors: Colors, private val context: Contex val binding = MmsFileListItemBinding.bind(holder.containerView) BubbleUtils.getBubble(false, canGroupWithPrevious, canGroupWithNext, message.isMe()) - .let(binding.fileBackground::setBackgroundResource) + .let(binding.fileBackground::setBackgroundResource) Observable.just(part.getUri()) - .map(context.contentResolver::openInputStream) - .map { inputStream -> inputStream.use { it.available() } } - .map { bytes -> - when (bytes) { - in 0..999 -> "$bytes B" - in 1000..999999 -> "${"%.1f".format(bytes / 1000f)} KB" - in 1000000..9999999 -> "${"%.1f".format(bytes / 1000000f)} MB" - else -> "${"%.1f".format(bytes / 1000000000f)} GB" - } + .map(context.contentResolver::openInputStream) + .map { inputStream -> inputStream.use { it.available() } } + .map { bytes -> + when (bytes) { + in 0..999 -> "$bytes B" + in 1000..999999 -> "${"%.1f".format(bytes / 1000f)} KB" + in 1000000..9999999 -> "${"%.1f".format(bytes / 1000000f)} MB" + else -> "${"%.1f".format(bytes / 1000000000f)} GB" } - .onErrorReturn { context.getString(R.string.compose_file_size_unavailable) } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { size -> binding.size.text = size } + } + .onErrorReturn { context.getString(R.string.compose_file_size_unavailable) } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { size -> binding.size.text = size } binding.filename.text = part.name @@ -83,7 +84,11 @@ class FileBinder @Inject constructor(colors: Colors, private val context: Contex binding.filename.setTextColor(theme.textPrimary) binding.size.setTextColor(theme.textTertiary) } else { - binding.fileBackground.setBackgroundTint(holder.containerView.context.resolveThemeColor(R.attr.bubbleColor)) + binding.fileBackground.setBackgroundTint( + holder.containerView.context.resolveThemeColor( + R.attr.bubbleColor + ) + ) binding.icon.setTint(holder.containerView.context.resolveThemeColor(android.R.attr.textColorSecondary)) binding.filename.setTextColor(holder.containerView.context.resolveThemeColor(android.R.attr.textColorPrimary)) binding.size.setTextColor(holder.containerView.context.resolveThemeColor(android.R.attr.textColorTertiary)) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/PartBinder.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/PartBinder.kt index 34a2bb222..417e70738 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/PartBinder.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/PartBinder.kt @@ -18,12 +18,12 @@ */ package org.prauga.messages.feature.compose.part +import io.reactivex.subjects.PublishSubject +import io.reactivex.subjects.Subject import org.prauga.messages.common.base.QkViewHolder import org.prauga.messages.common.util.Colors import org.prauga.messages.model.Message import org.prauga.messages.model.MmsPart -import io.reactivex.subjects.PublishSubject -import io.reactivex.subjects.Subject abstract class PartBinder { diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/PartsAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/PartsAdapter.kt index 454f2557d..db458f86d 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/PartsAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/PartsAdapter.kt @@ -21,6 +21,7 @@ package org.prauga.messages.feature.compose.part import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import io.reactivex.Observable import org.prauga.messages.R import org.prauga.messages.common.base.QkViewHolder import org.prauga.messages.common.util.Colors @@ -31,7 +32,6 @@ import org.prauga.messages.feature.compose.BubbleUtils.canGroup import org.prauga.messages.feature.compose.MessagesAdapter import org.prauga.messages.model.Message import org.prauga.messages.model.MmsPart -import io.reactivex.Observable import javax.inject.Inject @@ -69,19 +69,23 @@ class PartsAdapter @Inject constructor( this.message = message this.previous = previous this.next = next - this.bodyVisible = holder.containerView.findViewById(R.id.body)?.visibility == View.VISIBLE + this.bodyVisible = + holder.containerView.findViewById(R.id.body)?.visibility == View.VISIBLE this.data = message.parts.filter { !it.isSmil() && !it.isText() } this.audioState = audioState } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) - : QkContextMenuRecyclerView.ViewHolder { + : QkContextMenuRecyclerView.ViewHolder { val layout = partBinders.getOrNull(viewType)?.partLayout ?: 0 val view = LayoutInflater.from(parent.context).inflate(layout, parent, false) return QkContextMenuRecyclerView.ViewHolder(view) } - override fun onBindViewHolder(holder: QkContextMenuRecyclerView.ViewHolder, position: Int) { + override fun onBindViewHolder( + holder: QkContextMenuRecyclerView.ViewHolder, + position: Int + ) { val part = data[position] holder.contextMenuValue = part diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/VCardBinder.kt b/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/VCardBinder.kt index 3db8fcb17..dd0be654c 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/VCardBinder.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/compose/part/VCardBinder.kt @@ -20,6 +20,10 @@ package org.prauga.messages.feature.compose.part import android.content.Context import androidx.core.view.isVisible +import ezvcard.Ezvcard +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers import org.prauga.messages.R import org.prauga.messages.common.base.QkViewHolder import org.prauga.messages.common.util.Colors @@ -33,10 +37,6 @@ import org.prauga.messages.extensions.mapNotNull import org.prauga.messages.feature.compose.BubbleUtils import org.prauga.messages.model.Message import org.prauga.messages.model.MmsPart -import ezvcard.Ezvcard -import io.reactivex.Observable -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.schedulers.Schedulers import javax.inject.Inject class VCardBinder @Inject constructor(colors: Colors, private val context: Context) : PartBinder() { @@ -56,20 +56,20 @@ class VCardBinder @Inject constructor(colors: Colors, private val context: Conte val binding = MmsVcardListItemBinding.bind(holder.containerView) BubbleUtils.getBubble(false, canGroupWithPrevious, canGroupWithNext, message.isMe()) - .let(binding.vCardBackground::setBackgroundResource) + .let(binding.vCardBackground::setBackgroundResource) holder.containerView.setOnClickListener { clicks.onNext(part.id) } Observable.just(part.getUri()) - .map(context.contentResolver::openInputStream) - .mapNotNull { inputStream -> inputStream.use { Ezvcard.parse(it).first() } } - .map { vcard -> vcard.getDisplayName() ?: "" } - .subscribeOn(Schedulers.computation()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { displayName -> - binding.name?.text = displayName - binding.name.isVisible = displayName.isNotEmpty() - } + .map(context.contentResolver::openInputStream) + .mapNotNull { inputStream -> inputStream.use { Ezvcard.parse(it).first() } } + .map { vcard -> vcard.getDisplayName() ?: "" } + .subscribeOn(Schedulers.computation()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { displayName -> + binding.name?.text = displayName + binding.name.isVisible = displayName.isNotEmpty() + } if (!message.isMe()) { binding.vCardBackground.setBackgroundTint(theme.theme) @@ -77,7 +77,11 @@ class VCardBinder @Inject constructor(colors: Colors, private val context: Conte binding.name.setTextColor(theme.textPrimary) binding.label.setTextColor(theme.textTertiary) } else { - binding.vCardBackground.setBackgroundTint(holder.containerView.context.resolveThemeColor(R.attr.bubbleColor)) + binding.vCardBackground.setBackgroundTint( + holder.containerView.context.resolveThemeColor( + R.attr.bubbleColor + ) + ) binding.vCardAvatar.setTint(holder.containerView.context.resolveThemeColor(android.R.attr.textColorSecondary)) binding.name.setTextColor(holder.containerView.context.resolveThemeColor(android.R.attr.textColorPrimary)) binding.label.setTextColor(holder.containerView.context.resolveThemeColor(android.R.attr.textColorTertiary)) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsViewModel.kt b/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsViewModel.kt index 754b909a3..5a4943929 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsViewModel.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/contacts/ContactsViewModel.kt @@ -21,6 +21,7 @@ package org.prauga.messages.feature.contacts import android.view.inputmethod.EditorInfo import com.uber.autodispose.android.lifecycle.scope import com.uber.autodispose.autoDisposable +import com.uber.autodispose.autoDispose import org.prauga.messages.common.base.QkViewModel import org.prauga.messages.extensions.mapNotNull import org.prauga.messages.extensions.removeAccents @@ -84,132 +85,140 @@ class ContactsViewModel @Inject constructor( // Update the state's query, so we know if we should show the cancel button view.queryChangedIntent - .autoDisposable(view.scope()) - .subscribe { query -> newState { copy(query = query.toString()) } } + .autoDispose(view.scope()) + .subscribe { query -> newState { copy(query = query.toString()) } } // Clear the query view.queryClearedIntent - .autoDisposable(view.scope()) - .subscribe { view.clearQuery() } + .autoDispose(view.scope()) + .subscribe { view.clearQuery() } // Update the list of contact suggestions based on the query input, while also filtering out any contacts // that have already been selected Observables - .combineLatest( - view.queryChangedIntent, recents, starredContacts, contactGroups, contacts, selectedChips - ) { query, recents, starredContacts, contactGroups, contacts, selectedChips -> - val composeItems = mutableListOf() - if (query.isBlank()) { - composeItems += recents - .filter { conversation -> - conversation.recipients.any { recipient -> - selectedChips.none { chip -> - if (recipient.contact == null) { - chip.address == recipient.address - } else { - chip.contact?.lookupKey == recipient.contact?.lookupKey - } - } + .combineLatest( + view.queryChangedIntent, + recents, + starredContacts, + contactGroups, + contacts, + selectedChips + ) { query, recents, starredContacts, contactGroups, contacts, selectedChips -> + val composeItems = mutableListOf() + if (query.isBlank()) { + composeItems += recents + .filter { conversation -> + conversation.recipients.any { recipient -> + selectedChips.none { chip -> + if (recipient.contact == null) { + chip.address == recipient.address + } else { + chip.contact?.lookupKey == recipient.contact?.lookupKey } } - .map(ComposeItem::Recent) - - composeItems += starredContacts - .filter { contact -> selectedChips.none { it.contact?.lookupKey == contact.lookupKey } } - .map(ComposeItem::Starred) + } + } + .map(ComposeItem::Recent) - composeItems += contactGroups - .filter { group -> - group.contacts.any { contact -> - selectedChips.none { chip -> chip.contact?.lookupKey == contact.lookupKey } - } - } - .map(ComposeItem::Group) + composeItems += starredContacts + .filter { contact -> selectedChips.none { it.contact?.lookupKey == contact.lookupKey } } + .map(ComposeItem::Starred) - composeItems += contacts - .filter { contact -> selectedChips.none { it.contact?.lookupKey == contact.lookupKey } } - .map(ComposeItem::Person) - } else { - // If the entry is a valid destination, allow it as a recipient - if (phoneNumberUtils.isPossibleNumber(query.toString())) { - val newAddress = phoneNumberUtils.formatNumber(query) - val newContact = Contact(numbers = RealmList(PhoneNumber(address = newAddress))) - composeItems += ComposeItem.New(newContact) + composeItems += contactGroups + .filter { group -> + group.contacts.any { contact -> + selectedChips.none { chip -> chip.contact?.lookupKey == contact.lookupKey } + } } - - // Strip the accents from the query. This can be an expensive operation, so - // cache the result instead of doing it for each contact - val normalizedQuery = query.removeAccents() - composeItems += starredContacts - .asSequence() - .filter { contact -> selectedChips.none { it.contact?.lookupKey == contact.lookupKey } } - .filter { contact -> contactFilter.filter(contact, normalizedQuery) } - .map(ComposeItem::Starred) - - composeItems += contactGroups - .asSequence() - .filter { group -> - group.contacts.any { contact -> - selectedChips.none { chip -> chip.contact?.lookupKey == contact.lookupKey } - } - } - .filter { group -> contactGroupFilter.filter(group, normalizedQuery) } - .map(ComposeItem::Group) - - composeItems += contacts - .asSequence() - .filter { contact -> selectedChips.none { it.contact?.lookupKey == contact.lookupKey } } - .filter { contact -> contactFilter.filter(contact, normalizedQuery) } - .map(ComposeItem::Person) + .map(ComposeItem::Group) + + composeItems += contacts + .filter { contact -> selectedChips.none { it.contact?.lookupKey == contact.lookupKey } } + .map(ComposeItem::Person) + } else { + // If the entry is a valid destination, allow it as a recipient + if (phoneNumberUtils.isPossibleNumber(query.toString())) { + val newAddress = phoneNumberUtils.formatNumber(query) + val newContact = + Contact(numbers = RealmList(PhoneNumber(address = newAddress))) + composeItems += ComposeItem.New(newContact) } - composeItems + // Strip the accents from the query. This can be an expensive operation, so + // cache the result instead of doing it for each contact + val normalizedQuery = query.removeAccents() + composeItems += starredContacts + .asSequence() + .filter { contact -> selectedChips.none { it.contact?.lookupKey == contact.lookupKey } } + .filter { contact -> contactFilter.filter(contact, normalizedQuery) } + .map(ComposeItem::Starred) + + composeItems += contactGroups + .asSequence() + .filter { group -> + group.contacts.any { contact -> + selectedChips.none { chip -> chip.contact?.lookupKey == contact.lookupKey } + } + } + .filter { group -> contactGroupFilter.filter(group, normalizedQuery) } + .map(ComposeItem::Group) + + composeItems += contacts + .asSequence() + .filter { contact -> selectedChips.none { it.contact?.lookupKey == contact.lookupKey } } + .filter { contact -> contactFilter.filter(contact, normalizedQuery) } + .map(ComposeItem::Person) } - .subscribeOn(Schedulers.computation()) - .autoDisposable(view.scope()) - .subscribe { items -> newState { copy(composeItems = items) } } + + composeItems + } + .subscribeOn(Schedulers.computation()) + .autoDispose(view.scope()) + .subscribe { items -> newState { copy(composeItems = items) } } // Listen for ComposeItems being selected, and then send them off to the number picker dialog in case // the user needs to select a phone number view.queryEditorActionIntent - .filter { actionId -> actionId == EditorInfo.IME_ACTION_DONE } - .withLatestFrom(state) { _, state -> state } - .mapNotNull { state -> state.composeItems.firstOrNull() } - .mergeWith(view.composeItemPressedIntent) - .map { composeItem -> composeItem to false } - .mergeWith(view.composeItemLongPressedIntent.map { composeItem -> composeItem to true }) - .observeOn(Schedulers.io()) - .map { (composeItem, force) -> - HashMap(composeItem.getContacts().associate { contact -> - if (contact.numbers.size == 1 || contact.getDefaultNumber() != null && !force) { - val address = contact.getDefaultNumber()?.address ?: contact.numbers[0]!!.address - address to contact.lookupKey - } else { - runBlocking { - newState { copy(selectedContact = contact) } - val action = view.phoneNumberActionIntent.awaitFirst() - newState { copy(selectedContact = null) } - val numberId = view.phoneNumberSelectedIntent.awaitFirst().value - val number = contact.numbers.find { number -> number.id == numberId } - - if (action == PhoneNumberAction.CANCEL || number == null) { - return@runBlocking null - } - - if (action == PhoneNumberAction.ALWAYS) { - val params = SetDefaultPhoneNumber.Params(contact.lookupKey, number.id) - setDefaultPhoneNumber.execute(params) - } - - number.address to contact.lookupKey - } ?: return@map hashMapOf() - } - }) - } - .filter { result -> result.isNotEmpty() } - .observeOn(AndroidSchedulers.mainThread()) - .autoDisposable(view.scope()) - .subscribe { result -> view.finish(result) } + .filter { actionId -> actionId == EditorInfo.IME_ACTION_DONE } + .withLatestFrom(state) { _, state -> state } + .mapNotNull { state -> state.composeItems.firstOrNull() } + .mergeWith(view.composeItemPressedIntent) + .map { composeItem -> composeItem to false } + .mergeWith(view.composeItemLongPressedIntent.map { composeItem -> composeItem to true }) + .observeOn(Schedulers.io()) + .map { (composeItem, force) -> + HashMap(composeItem.getContacts().associate { contact -> + if (contact.numbers.size == 1 || contact.getDefaultNumber() != null && !force) { + val address = + contact.getDefaultNumber()?.address ?: contact.numbers[0]!!.address + address to contact.lookupKey + } else { + runBlocking { + newState { copy(selectedContact = contact) } + val action = view.phoneNumberActionIntent.awaitFirst() + newState { copy(selectedContact = null) } + val numberId = view.phoneNumberSelectedIntent.awaitFirst().value + val number = contact.numbers.find { number -> number.id == numberId } + + if (action == PhoneNumberAction.CANCEL || number == null) { + return@runBlocking null + } + + if (action == PhoneNumberAction.ALWAYS) { + val params = + SetDefaultPhoneNumber.Params(contact.lookupKey, number.id) + setDefaultPhoneNumber.execute(params) + } + + number.address to contact.lookupKey + } ?: return@map hashMapOf() + } + }) + } + .filter { result -> result.isNotEmpty() } + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(view.scope()) + .subscribe { result -> view.finish(result) } } } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoController.kt b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoController.kt index 30944adc4..991e05817 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoController.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoController.kt @@ -38,6 +38,7 @@ import io.reactivex.Observable import io.reactivex.subjects.PublishSubject import io.reactivex.subjects.Subject import androidx.recyclerview.widget.RecyclerView +import com.uber.autodispose.autoDispose import javax.inject.Inject class ConversationInfoController( @@ -84,8 +85,8 @@ class ConversationInfoController( } themedActivity?.theme - ?.autoDisposable(scope()) - ?.subscribe { recyclerView.scrapViews() } + ?.autoDispose(scope()) + ?.subscribe { recyclerView.scrapViews() } } override fun onAttach(view: View) { @@ -144,10 +145,10 @@ class ConversationInfoController( dialog.show() themedActivity?.theme?.take(1) - ?.autoDisposable(scope()) - ?.subscribe { theme -> - dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.setTextColor(theme.theme) - dialog.getButton(AlertDialog.BUTTON_NEGATIVE)?.setTextColor(theme.theme) - } + ?.autoDispose(scope()) + ?.subscribe { theme -> + dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.setTextColor(theme.theme) + dialog.getButton(AlertDialog.BUTTON_NEGATIVE)?.setTextColor(theme.theme) + } } } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoPresenter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoPresenter.kt index 120470dc8..9ab6eb1b8 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoPresenter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/conversationinfo/ConversationInfoPresenter.kt @@ -22,6 +22,7 @@ import android.content.Context import androidx.lifecycle.Lifecycle import com.uber.autodispose.android.lifecycle.scope import com.uber.autodispose.autoDisposable +import com.uber.autodispose.autoDispose import org.prauga.messages.R import org.prauga.messages.common.Navigator import org.prauga.messages.common.base.QkPresenter @@ -112,95 +113,100 @@ class ConversationInfoPresenter @Inject constructor( // Add or display the contact view.recipientClicks() - .mapNotNull(conversationRepo::getRecipient) - .doOnNext { recipient -> - recipient.contact?.lookupKey?.let(navigator::showContact) - ?: navigator.addContact(recipient.address) - } - .autoDisposable(view.scope(Lifecycle.Event.ON_DESTROY)) // ... this should be the default - .subscribe() + .mapNotNull(conversationRepo::getRecipient) + .doOnNext { recipient -> + recipient.contact?.lookupKey?.let(navigator::showContact) + ?: navigator.addContact(recipient.address) + } + .autoDispose(view.scope(Lifecycle.Event.ON_DESTROY)) // ... this should be the default + .subscribe() // Copy phone number view.recipientLongClicks() - .mapNotNull(conversationRepo::getRecipient) - .map { recipient -> recipient.address } - .observeOn(AndroidSchedulers.mainThread()) - .autoDisposable(view.scope()) - .subscribe { address -> - ClipboardUtils.copy(context, address) - context.makeToast(R.string.info_copied_address) - } + .mapNotNull(conversationRepo::getRecipient) + .map { recipient -> recipient.address } + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(view.scope()) + .subscribe { address -> + ClipboardUtils.copy(context, address) + context.makeToast(R.string.info_copied_address) + } // Show the theme settings for the conversation view.themeClicks() - .autoDisposable(view.scope()) - .subscribe(view::showThemePicker) + .autoDispose(view.scope()) + .subscribe(view::showThemePicker) // Show the conversation title dialog view.nameClicks() - .withLatestFrom(conversation) { _, conversation -> conversation } - .map { conversation -> conversation.name } - .autoDisposable(view.scope()) - .subscribe(view::showNameDialog) + .withLatestFrom(conversation) { _, conversation -> conversation } + .map { conversation -> conversation.name } + .autoDispose(view.scope()) + .subscribe(view::showNameDialog) // Set the conversation title view.nameChanges() - .withLatestFrom(conversation) { name, conversation -> - conversationRepo.setConversationName(conversation.id, name) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - } - .flatMapCompletable { it } - .autoDisposable(view.scope()) - .subscribe() + .withLatestFrom(conversation) { name, conversation -> + conversationRepo.setConversationName(conversation.id, name) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + } + .flatMapCompletable { it } + .autoDispose(view.scope()) + .subscribe() // Show the notifications settings for the conversation view.notificationClicks() - .withLatestFrom(conversation) { _, conversation -> conversation } - .autoDisposable(view.scope()) - .subscribe { conversation -> navigator.showNotificationSettings(conversation.id) } + .withLatestFrom(conversation) { _, conversation -> conversation } + .autoDispose(view.scope()) + .subscribe { conversation -> navigator.showNotificationSettings(conversation.id) } view.markUnreadClicks() - .withLatestFrom(conversation) { _, conversation -> conversation } - .autoDisposable(view.scope()) - .subscribe {conversation -> - markUnread.execute(listOf(conversation.id)) - navigator.showMainActivity() - } + .withLatestFrom(conversation) { _, conversation -> conversation } + .autoDispose(view.scope()) + .subscribe { conversation -> + markUnread.execute(listOf(conversation.id)) + navigator.showMainActivity() + } // Toggle the archived state of the conversation view.archiveClicks() - .withLatestFrom(conversation) { _, conversation -> conversation } - .autoDisposable(view.scope()) - .subscribe { conversation -> - when (conversation.archived) { - true -> markUnarchived.execute(listOf(conversation.id)) - false -> markArchived.execute(listOf(conversation.id)) - } + .withLatestFrom(conversation) { _, conversation -> conversation } + .autoDispose(view.scope()) + .subscribe { conversation -> + when (conversation.archived) { + true -> markUnarchived.execute(listOf(conversation.id)) + false -> markArchived.execute(listOf(conversation.id)) } + } // Toggle the blocked state of the conversation view.blockClicks() - .withLatestFrom(conversation) { _, conversation -> conversation } - .autoDisposable(view.scope()) - .subscribe { conversation -> view.showBlockingDialog(listOf(conversation.id), !conversation.blocked) } + .withLatestFrom(conversation) { _, conversation -> conversation } + .autoDispose(view.scope()) + .subscribe { conversation -> + view.showBlockingDialog( + listOf(conversation.id), + !conversation.blocked + ) + } // Show the delete confirmation dialog view.deleteClicks() - .filter { permissionManager.isDefaultSms().also { if (!it) view.requestDefaultSms() } } - .autoDisposable(view.scope()) - .subscribe { view.showDeleteDialog() } + .filter { permissionManager.isDefaultSms().also { if (!it) view.requestDefaultSms() } } + .autoDispose(view.scope()) + .subscribe { view.showDeleteDialog() } // Delete the conversation view.confirmDelete() - .withLatestFrom(conversation) { _, conversation -> conversation } - .autoDisposable(view.scope()) - .subscribe { conversation -> deleteConversations.execute(listOf(conversation.id)) } + .withLatestFrom(conversation) { _, conversation -> conversation } + .autoDispose(view.scope()) + .subscribe { conversation -> deleteConversations.execute(listOf(conversation.id)) } // Media view.mediaClicks() - .autoDisposable(view.scope()) - .subscribe(navigator::showMedia) + .autoDispose(view.scope()) + .subscribe(navigator::showMedia) } } \ No newline at end of file diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/gallery/GalleryActivity.kt b/presentation/src/main/java/com/moez/QKSMS/feature/gallery/GalleryActivity.kt index 1db27f904..11c628774 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/gallery/GalleryActivity.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/gallery/GalleryActivity.kt @@ -28,21 +28,24 @@ import androidx.core.app.ActivityCompat import androidx.core.view.isVisible import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProviders +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView import androidx.viewpager2.widget.ViewPager2 import dagger.android.AndroidInjection +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.rx2.asFlow import org.prauga.messages.R import org.prauga.messages.common.base.QkActivity import org.prauga.messages.common.util.DateFormatter import org.prauga.messages.common.util.extensions.setVisible import org.prauga.messages.databinding.GalleryActivityBinding import org.prauga.messages.model.MmsPart -import io.reactivex.Observable -import io.reactivex.subjects.PublishSubject -import io.reactivex.subjects.Subject import javax.inject.Inject -class GalleryActivity : QkActivity(GalleryActivityBinding::inflate), +class GalleryActivity : + QkActivity(GalleryActivityBinding::inflate), GalleryView { @Inject @@ -56,8 +59,10 @@ class GalleryActivity : QkActivity(GalleryActivityBindin val partId by lazy { intent.getLongExtra("partId", 0L) } - private val optionsItemSubject: Subject = PublishSubject.create() - private val pageChangedSubject: Subject = PublishSubject.create() + private val _optionsItemSelected = MutableSharedFlow(extraBufferCapacity = 1) + private val _pageChanged = MutableSharedFlow(extraBufferCapacity = 1) + private val _screenTouched = MutableSharedFlow(extraBufferCapacity = 1) + private val viewModel by lazy { ViewModelProviders.of( this, @@ -91,14 +96,28 @@ class GalleryActivity : QkActivity(GalleryActivityBindin } } }) + + lifecycleScope.launch { + pagerAdapter.clicks + .asFlow() + .collect { + _screenTouched.emit(Unit) + } + } } fun onPageSelected(position: Int) { + val part = pagerAdapter.getItem(position) + binding.toolbarSubtitle.text = pagerAdapter.getItem(position)?.messages?.firstOrNull()?.date ?.let(dateFormatter::getDetailedTimestamp) binding.toolbarSubtitle.isVisible = binding.toolbarTitle.text.isNotBlank() - pagerAdapter.getItem(position)?.run(pageChangedSubject::onNext) + if (part != null) { + lifecycleScope.launch { + _pageChanged.emit(part) + } + } } override fun render(state: GalleryState) { @@ -108,11 +127,9 @@ class GalleryActivity : QkActivity(GalleryActivityBindin pagerAdapter.updateData(state.parts) } - override fun optionsItemSelected(): Observable = optionsItemSubject - - override fun screenTouched(): Observable<*> = pagerAdapter.clicks - - override fun pageChanged(): Observable = pageChangedSubject + override fun optionsItemSelected(): Flow = _optionsItemSelected + override fun screenTouched(): Flow = _screenTouched + override fun pageChanged(): Flow = _pageChanged override fun requestStoragePermission() { ActivityCompat.requestPermissions( @@ -130,7 +147,9 @@ class GalleryActivity : QkActivity(GalleryActivityBindin override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { android.R.id.home -> onBackPressed() - else -> optionsItemSubject.onNext(item.itemId) + else -> lifecycleScope.launch { + _optionsItemSelected.emit(item.itemId) + } } return true } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/gallery/GalleryPagerAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/gallery/GalleryPagerAdapter.kt index 3e78e60ab..e081e2c66 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/gallery/GalleryPagerAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/gallery/GalleryPagerAdapter.kt @@ -30,6 +30,8 @@ import com.google.android.exoplayer2.trackselection.DefaultTrackSelector import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory import com.google.android.exoplayer2.util.Util import com.google.android.mms.ContentType +import io.reactivex.subjects.PublishSubject +import io.reactivex.subjects.Subject import org.prauga.messages.R import org.prauga.messages.common.base.QkRealmAdapter import org.prauga.messages.common.base.QkViewHolder @@ -39,12 +41,12 @@ import org.prauga.messages.extensions.isImage import org.prauga.messages.extensions.isVideo import org.prauga.messages.model.MmsPart import org.prauga.messages.util.GlideApp -import io.reactivex.subjects.PublishSubject -import io.reactivex.subjects.Subject -import java.util.* +import java.util.Collections +import java.util.WeakHashMap import javax.inject.Inject -class GalleryPagerAdapter @Inject constructor(private val context: Context) : QkRealmAdapter() { +class GalleryPagerAdapter @Inject constructor(private val context: Context) : + QkRealmAdapter() { companion object { private const val VIEW_TYPE_INVALID = 0 @@ -59,7 +61,8 @@ class GalleryPagerAdapter @Inject constructor(private val context: Context) : Qk override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QkViewHolder { val inflater = LayoutInflater.from(parent.context) - return QkViewHolder(when (viewType) { + return QkViewHolder( + when (viewType) { VIEW_TYPE_IMAGE -> { val binding = GalleryImagePageBinding.inflate(inflater, parent, false) // When calling the public setter, it doesn't allow the midscale to be the same as the @@ -101,14 +104,14 @@ class GalleryPagerAdapter @Inject constructor(private val context: Context) : Qk // We need to explicitly request a gif from glide for animations to work when (part.getUri().let(contentResolver::getType)) { ContentType.IMAGE_GIF -> GlideApp.with(context) - .asGif() - .load(part.getUri()) - .into(binding.image) + .asGif() + .load(part.getUri()) + .into(binding.image) else -> GlideApp.with(context) - .asBitmap() - .load(part.getUri()) - .into(binding.image) + .asBitmap() + .load(part.getUri()) + .into(binding.image) } } @@ -120,8 +123,10 @@ class GalleryPagerAdapter @Inject constructor(private val context: Context) : Qk binding.video.player = exoPlayer exoPlayers.add(exoPlayer) - val dataSourceFactory = DefaultDataSourceFactory(context, Util.getUserAgent(context, "QUIK")) - val videoSource = ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(part.getUri()) + val dataSourceFactory = + DefaultDataSourceFactory(context, Util.getUserAgent(context, "QUIK")) + val videoSource = + ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(part.getUri()) exoPlayer?.prepare(videoSource) } } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/gallery/GalleryState.kt b/presentation/src/main/java/com/moez/QKSMS/feature/gallery/GalleryState.kt index 14f98a373..b415f46bd 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/gallery/GalleryState.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/gallery/GalleryState.kt @@ -18,8 +18,8 @@ */ package org.prauga.messages.feature.gallery -import org.prauga.messages.model.MmsPart import io.realm.RealmResults +import org.prauga.messages.model.MmsPart data class GalleryState( val navigationVisible: Boolean = true, diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/gallery/GalleryView.kt b/presentation/src/main/java/com/moez/QKSMS/feature/gallery/GalleryView.kt index 057248a42..81b57bb12 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/gallery/GalleryView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/gallery/GalleryView.kt @@ -1,5 +1,6 @@ /* * Copyright (C) 2017 Moez Bhatti + * Copyright (C) 2025 Saalim Quadri * * This file is part of QKSMS. * @@ -16,18 +17,17 @@ * You should have received a copy of the GNU General Public License * along with QKSMS. If not, see . */ + package org.prauga.messages.feature.gallery +import kotlinx.coroutines.flow.Flow import org.prauga.messages.common.base.QkView import org.prauga.messages.model.MmsPart -import io.reactivex.Observable interface GalleryView : QkView { - fun optionsItemSelected(): Observable - fun screenTouched(): Observable<*> - fun pageChanged(): Observable - + fun optionsItemSelected(): Flow + fun screenTouched(): Flow + fun pageChanged(): Flow fun requestStoragePermission() - } \ No newline at end of file diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/gallery/GalleryViewModel.kt b/presentation/src/main/java/com/moez/QKSMS/feature/gallery/GalleryViewModel.kt index 662639ebb..6060ad806 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/gallery/GalleryViewModel.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/gallery/GalleryViewModel.kt @@ -1,5 +1,6 @@ /* * Copyright (C) 2017 Moez Bhatti + * Copyright (C) 2025 Saalim Quadri * * This file is part of QKSMS. * @@ -16,23 +17,26 @@ * You should have received a copy of the GNU General Public License * along with QKSMS. If not, see . */ + package org.prauga.messages.feature.gallery import android.content.Context +import androidx.lifecycle.viewModelScope +import com.moez.QKSMS.common.base.PvotViewModel import com.moez.QKSMS.contentproviders.MmsPartProvider -import com.uber.autodispose.android.lifecycle.scope -import com.uber.autodispose.autoDisposable +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.launch import org.prauga.messages.R import org.prauga.messages.common.Navigator -import org.prauga.messages.common.base.QkViewModel import org.prauga.messages.common.util.extensions.makeToast -import org.prauga.messages.extensions.mapNotNull import org.prauga.messages.interactor.SaveImage import org.prauga.messages.manager.PermissionManager +import org.prauga.messages.model.MmsPart import org.prauga.messages.repository.ConversationRepository import org.prauga.messages.repository.MessageRepository -import io.reactivex.Flowable -import io.reactivex.rxkotlin.plusAssign import javax.inject.Inject import javax.inject.Named @@ -44,72 +48,103 @@ class GalleryViewModel @Inject constructor( private val navigator: Navigator, private val saveImage: SaveImage, private val permissions: PermissionManager -) : QkViewModel(GalleryState()) { +) : PvotViewModel(GalleryState()) { + companion object { const val DEFAULT_SHARE_FILENAME = "quik-media-attachment.jpg" } + private var latestPart: MmsPart? = null + init { - disposables += Flowable.just(partId) - .mapNotNull(messageRepo::getMessageForPart) - .mapNotNull { message -> message.threadId } - .doOnNext { threadId -> newState { copy(parts = messageRepo.getPartsForConversation(threadId)) } } - .doOnNext { threadId -> - newState { - copy(title = conversationRepo.getConversation(threadId)?.getTitle()) - } - } - .subscribe() + viewModelScope.launch { + val message = messageRepo.getMessageForPart(partId) ?: return@launch + val threadId = message.threadId ?: return@launch + val parts = messageRepo.getPartsForConversation(threadId) + val title = conversationRepo.getConversation(threadId)?.getTitle() + + newState { copy(parts = parts) } + newState { copy(title = title) } + } } - override fun bindView(view: GalleryView) { + fun bindView(view: GalleryView) { super.bindView(view) + // share options menu stream + val optionsFlow: SharedFlow = + view.optionsItemSelected() + .shareIn(viewModelScope, started = SharingStarted.Eagerly, replay = 0) + + viewModelScope.launch { + view.pageChanged() + .collect { part -> + latestPart = part + } + } + // When the screen is touched, toggle the visibility of the navigation UI - view.screenTouched() - .withLatestFrom(state) { _, state -> state.navigationVisible } - .map { navigationVisible -> !navigationVisible } - .autoDisposable(view.scope()) - .subscribe { navigationVisible -> newState { copy(navigationVisible = navigationVisible) } } + viewModelScope.launch { + view.screenTouched() + .collect { + val current = state.value + newState { copy(navigationVisible = !current.navigationVisible) } + } + } // Save image to device - view.optionsItemSelected() + viewModelScope.launch { + optionsFlow .filter { it == R.id.save } - .filter { permissions.hasStorage().also { if (!it) view.requestStoragePermission() } } - .withLatestFrom(view.pageChanged()) { _, part -> part.id } - .autoDisposable(view.scope()) - .subscribe { partId -> saveImage.execute(partId) { context.makeToast(R.string.gallery_toast_saved) } } + .collect { + val part = latestPart ?: return@collect + + if (!permissions.hasStorage()) { + view.requestStoragePermission() + return@collect + } + saveImage.execute(part.id) { + context.makeToast(R.string.gallery_toast_saved) + } + } + } // Share image externally - view.optionsItemSelected() + viewModelScope.launch { + optionsFlow .filter { it == R.id.share } - .withLatestFrom(view.pageChanged()) { _, part -> part } - .autoDisposable(view.scope()) - .subscribe { + .collect { + val part = latestPart ?: return@collect + navigator.shareFile( - MmsPartProvider.getUriForMmsPartId(it.id, it.getBestFilename()), - it.type + MmsPartProvider.getUriForMmsPartId(part.id, part.getBestFilename()), + part.type ) } + } // message part context menu item selected - forward - view.optionsItemSelected() - .filter { it == R.id.forward } - .withLatestFrom(view.pageChanged()) { _, part -> part } - .autoDisposable(view.scope()) - .subscribe { navigator.showCompose("", listOf(it.getUri())) } + viewModelScope.launch { + optionsFlow + .filter { it == R.id.forward } + .collect { + val part = latestPart ?: return@collect + navigator.showCompose("", listOf(part.getUri())) + } + } // message part context menu item selected - open externally - view.optionsItemSelected() - .filter { it == R.id.openExternally } - .withLatestFrom(view.pageChanged()) { _, part -> part } - .autoDisposable(view.scope()) - .subscribe { - navigator.viewFile( - MmsPartProvider.getUriForMmsPartId(it.id, it.getBestFilename()), - it.type - ) - } - } + viewModelScope.launch { + optionsFlow + .filter { it == R.id.openExternally } + .collect { + val part = latestPart ?: return@collect + navigator.viewFile( + MmsPartProvider.getUriForMmsPartId(part.id, part.getBestFilename()), + part.type + ) + } + } + } } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/main/DrawerBadgesExperiment.kt b/presentation/src/main/java/com/moez/QKSMS/feature/main/DrawerBadgesExperiment.kt index 9a24d99fe..54526c740 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/main/DrawerBadgesExperiment.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/main/DrawerBadgesExperiment.kt @@ -30,8 +30,9 @@ class DrawerBadgesExperiment @Inject constructor( override val key: String = "Drawer Badges" override val variants: List> = listOf( - Variant("variant_a", false), - Variant("variant_b", true)) + Variant("variant_a", false), + Variant("variant_b", true) + ) override val default: Boolean = false diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/main/MainActivity.kt b/presentation/src/main/java/com/moez/QKSMS/feature/main/MainActivity.kt index 694e22e52..1b083a62d 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/main/MainActivity.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/main/MainActivity.kt @@ -36,6 +36,7 @@ import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.PopupMenu import androidx.core.app.ActivityCompat import androidx.core.view.GravityCompat +import androidx.core.view.get import androidx.core.view.isVisible import androidx.core.view.size import androidx.lifecycle.Lifecycle @@ -47,7 +48,7 @@ import com.google.android.material.snackbar.Snackbar import com.jakewharton.rxbinding2.view.clicks import com.jakewharton.rxbinding2.widget.textChanges import com.uber.autodispose.android.lifecycle.scope -import com.uber.autodispose.autoDisposable +import com.uber.autodispose.autoDispose import dagger.android.AndroidInjection import io.reactivex.Observable import io.reactivex.disposables.CompositeDisposable @@ -73,7 +74,6 @@ import org.prauga.messages.feature.conversations.ConversationsAdapter import org.prauga.messages.manager.ChangelogManager import org.prauga.messages.repository.SyncRepository import javax.inject.Inject -import androidx.core.view.get class MainActivity : QkThemedActivity(MainActivityBinding::inflate), MainView { @@ -170,7 +170,7 @@ class MainActivity : QkThemedActivity(MainActivityBinding:: (binding.snackbar as? ViewStub)?.setOnInflateListener { _, inflated -> inflated.findViewById(R.id.snackbarButton).clicks() - .autoDisposable(scope(Lifecycle.Event.ON_DESTROY)) + .autoDispose(scope(Lifecycle.Event.ON_DESTROY)) .subscribe(snackbarButtonIntent) } @@ -193,7 +193,7 @@ class MainActivity : QkThemedActivity(MainActivityBinding:: } binding.cVTopBar3.clicks() - .autoDisposable(scope()) + .autoDispose(scope()) .subscribe { showDrawerMenu() } @@ -211,8 +211,10 @@ class MainActivity : QkThemedActivity(MainActivityBinding:: -binding.cVTopBar2.height.toFloat() - 8f * resources.displayMetrics.density binding.cVTopBar2.animate().translationY(translationY).setDuration(200).start() binding.cVTopBar3.animate().translationY(translationY).setDuration(200).start() - binding.filterGroup.animate().translationY(translationY).setDuration(200).start() - binding.recyclerView.animate().translationY(translationY).setDuration(200).start() + binding.filterGroup.animate().translationY(translationY).setDuration(200) + .start() + binding.recyclerView.animate().translationY(translationY).setDuration(200) + .start() } else if (dy < 0 && binding.cVTopBar2.translationY != 0f) { // Show binding.cVTopBar2.animate().translationY(0f).setDuration(200).start() @@ -224,11 +226,11 @@ class MainActivity : QkThemedActivity(MainActivityBinding:: }) // Don't allow clicks to pass through the drawer layout - binding.drawer.root.clicks().autoDisposable(scope()).subscribe() + binding.drawer.root.clicks().autoDispose(scope()).subscribe() // Set the theme color tint to the recyclerView, progressbar, and FAB theme - .autoDisposable(scope()) + .autoDispose(scope()) .subscribe { theme -> // Set the color for the drawer icons val states = arrayOf( @@ -580,7 +582,7 @@ class MainActivity : QkThemedActivity(MainActivityBinding:: dialog.show() theme.take(1) - .autoDisposable(scope()) + .autoDispose(scope()) .subscribe { theme -> dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.setTextColor(theme.theme) dialog.getButton(AlertDialog.BUTTON_NEGATIVE)?.setTextColor(theme.theme) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/main/MainActivityModule.kt b/presentation/src/main/java/com/moez/QKSMS/feature/main/MainActivityModule.kt index 617655417..cd1889b2d 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/main/MainActivityModule.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/main/MainActivityModule.kt @@ -22,9 +22,9 @@ import androidx.lifecycle.ViewModel import dagger.Module import dagger.Provides import dagger.multibindings.IntoMap +import io.reactivex.disposables.CompositeDisposable import org.prauga.messages.injection.ViewModelKey import org.prauga.messages.injection.scope.ActivityScope -import io.reactivex.disposables.CompositeDisposable @Module class MainActivityModule { diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/main/MainState.kt b/presentation/src/main/java/com/moez/QKSMS/feature/main/MainState.kt index c8555e621..8a6403d4d 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/main/MainState.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/main/MainState.kt @@ -19,10 +19,10 @@ package org.prauga.messages.feature.main +import io.realm.RealmResults import org.prauga.messages.model.Conversation import org.prauga.messages.model.SearchResult import org.prauga.messages.repository.SyncRepository -import io.realm.RealmResults data class MainState( val hasError: Boolean = false, diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/main/MainView.kt b/presentation/src/main/java/com/moez/QKSMS/feature/main/MainView.kt index da08ad8c7..70662c60b 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/main/MainView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/main/MainView.kt @@ -19,9 +19,9 @@ package org.prauga.messages.feature.main import android.content.Intent +import io.reactivex.Observable import org.prauga.messages.common.base.QkView import org.prauga.messages.manager.ChangelogManager -import io.reactivex.Observable interface MainView : QkView { @@ -34,7 +34,8 @@ interface MainView : QkView { val navigationIntent: Observable val optionsItemIntent: Observable val filterSelectedIntent: Observable -// val plusBannerIntent: Observable<*> + + // val plusBannerIntent: Observable<*> val dismissRatingIntent: Observable<*> val rateIntent: Observable<*> val conversationsSelectedIntent: Observable> diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/main/MainViewModel.kt b/presentation/src/main/java/com/moez/QKSMS/feature/main/MainViewModel.kt index 8f0d72d48..96251e013 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/main/MainViewModel.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/main/MainViewModel.kt @@ -20,11 +20,20 @@ package org.prauga.messages.feature.main import androidx.recyclerview.widget.ItemTouchHelper import com.uber.autodispose.android.lifecycle.scope -import com.uber.autodispose.autoDisposable +import com.uber.autodispose.autoDispose +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.rxkotlin.plusAssign +import io.reactivex.schedulers.Schedulers +import io.realm.Realm +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch import org.prauga.messages.R import org.prauga.messages.common.Navigator import org.prauga.messages.common.base.QkViewModel import org.prauga.messages.extensions.mapNotNull +import org.prauga.messages.feature.main.ConversationFilterType.ALL +import org.prauga.messages.feature.main.ConversationFilterType.UNREAD import org.prauga.messages.interactor.DeleteConversations import org.prauga.messages.interactor.MarkAllSeen import org.prauga.messages.interactor.MarkArchived @@ -37,8 +46,6 @@ import org.prauga.messages.interactor.MigratePreferences import org.prauga.messages.interactor.SpeakThreads import org.prauga.messages.interactor.SyncContacts import org.prauga.messages.interactor.SyncMessages -import org.prauga.messages.feature.main.ConversationFilterType.ALL -import org.prauga.messages.feature.main.ConversationFilterType.UNREAD import org.prauga.messages.listener.ContactAddedListener import org.prauga.messages.manager.BillingManager import org.prauga.messages.manager.ChangelogManager @@ -51,13 +58,6 @@ import org.prauga.messages.repository.EmojiReactionRepository import org.prauga.messages.repository.MessageRepository import org.prauga.messages.repository.SyncRepository import org.prauga.messages.util.Preferences -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.rxkotlin.plusAssign -import io.reactivex.schedulers.Schedulers -import io.realm.Realm -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch import java.util.Locale import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -123,17 +123,17 @@ class MainViewModel @Inject constructor( // Show the syncing UI disposables += syncRepository.syncProgress - .sample(16, TimeUnit.MILLISECONDS) - .distinctUntilChanged() - .subscribe { syncing -> newState { copy(syncing = syncing) } } + .sample(16, TimeUnit.MILLISECONDS) + .distinctUntilChanged() + .subscribe { syncing -> newState { copy(syncing = syncing) } } // Update the upgraded status disposables += billingManager.upgradeStatus - .subscribe { upgraded -> newState { copy(upgraded = upgraded) } } + .subscribe { upgraded -> newState { copy(upgraded = upgraded) } } // Show the rating UI disposables += ratingManager.shouldShowRating - .subscribe { show -> newState { copy(showRating = show) } } + .subscribe { show -> newState { copy(showRating = show) } } // Migrate the preferences from 2.7.3 @@ -142,7 +142,8 @@ class MainViewModel @Inject constructor( // If we have all permissions and we've never run a sync, run a sync. This will be the case // when upgrading from 2.7.3, or if the app's data was cleared - val lastSync = Realm.getDefaultInstance().use { realm -> realm.where(SyncLog::class.java)?.max("date") ?: 0 } + val lastSync = Realm.getDefaultInstance() + .use { realm -> realm.where(SyncLog::class.java)?.max("date") ?: 0 } if (lastSync == 0 && permissionManager.isDefaultSms() && permissionManager.hasReadSms() && permissionManager.hasContacts()) { syncMessages.execute(Unit) } @@ -159,9 +160,9 @@ class MainViewModel @Inject constructor( // Sync contacts when we detect a change if (permissionManager.hasContacts()) { disposables += contactAddedListener.listen() - .debounce(1, TimeUnit.SECONDS) - .subscribeOn(Schedulers.io()) - .subscribe { syncContacts.execute(Unit) } + .debounce(1, TimeUnit.SECONDS) + .subscribeOn(Schedulers.io()) + .subscribe { syncContacts.execute(Unit) } } ratingManager.addSession() @@ -192,7 +193,7 @@ class MainViewModel @Inject constructor( copy(page = Archived(data = archivedData(currentFilter))) } } - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe() view.activityResumedIntent @@ -220,7 +221,7 @@ class MainViewModel @Inject constructor( .map { permissionManager.hasReadSms() } .distinctUntilChanged() .doOnNext { smsPermission -> newState { copy(smsPermission = smsPermission) } } - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe() // If the Contacts Permission state changes, reflect it in the State @@ -230,7 +231,7 @@ class MainViewModel @Inject constructor( .map { permissionManager.hasContacts() } .distinctUntilChanged() .doOnNext { contactPermission -> newState { copy(contactPermission = contactPermission) } } - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe() // If the Notifications Permission state changes, reflect it in the State @@ -240,7 +241,7 @@ class MainViewModel @Inject constructor( .map { permissionManager.hasNotifications() } .distinctUntilChanged() .doOnNext { notificationPermission -> newState { copy(notificationPermission = notificationPermission) } } - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe() // If we go from not having all SMS permissions to having them, sync messages @@ -251,18 +252,18 @@ class MainViewModel @Inject constructor( .distinctUntilChanged() .skip(1) .filter { hasAllPermissions -> hasAllPermissions } - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe { syncMessages.execute(Unit) } // Launch screen from intent view.onNewIntentIntent - .autoDisposable(view.scope()) - .subscribe { intent -> - when (intent.getStringExtra("screen")) { - "compose" -> navigator.showConversation(intent.getLongExtra("threadId", 0)) - "blocking" -> navigator.showBlockedConversations() - } + .autoDispose(view.scope()) + .subscribe { intent -> + when (intent.getStringExtra("screen")) { + "compose" -> navigator.showConversation(intent.getLongExtra("threadId", 0)) + "blocking" -> navigator.showBlockedConversations() } + } // Show changelog if (changelogManager.didUpdate()) { @@ -280,35 +281,35 @@ class MainViewModel @Inject constructor( } view.changelogMoreIntent - .autoDisposable(view.scope()) - .subscribe { navigator.showChangelog() } + .autoDispose(view.scope()) + .subscribe { navigator.showChangelog() } view.queryChangedIntent - .debounce(200, TimeUnit.MILLISECONDS) - .observeOn(AndroidSchedulers.mainThread()) - .map { query -> query.trim() } - .withLatestFrom(state) { query, state -> - if (query.isEmpty() && state.page is Searching) { - newState { copy(page = Inbox(data = inboxData(currentFilter))) } - } - query + .debounce(200, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .map { query -> query.trim() } + .withLatestFrom(state) { query, state -> + if (query.isEmpty() && state.page is Searching) { + newState { copy(page = Inbox(data = inboxData(currentFilter))) } } - .filter { query -> query.length >= 2 } - .distinctUntilChanged() - .doOnNext { - newState { - val page = (page as? Searching) ?: Searching() - copy(page = page.copy(loading = true)) - } + query + } + .filter { query -> query.length >= 2 } + .distinctUntilChanged() + .doOnNext { + newState { + val page = (page as? Searching) ?: Searching() + copy(page = page.copy(loading = true)) } - .observeOn(Schedulers.io()) - .map(conversationRepo::searchConversations) - .autoDisposable(view.scope()) - .subscribe { data -> newState { copy(page = Searching(loading = false, data = data)) } } + } + .observeOn(Schedulers.io()) + .map(conversationRepo::searchConversations) + .autoDispose(view.scope()) + .subscribe { data -> newState { copy(page = Searching(loading = false, data = data)) } } view.filterSelectedIntent .distinctUntilChanged() - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe { chip -> when (chip) { MessageCategory.ALL -> newState { @@ -338,196 +339,199 @@ class MainViewModel @Inject constructor( } view.activityResumedIntent - .filter { resumed -> !resumed } - .switchMap { - // Take until the activity is resumed - prefs.keyChanges - .filter { key -> key.contains("theme") } - .map { true } - .doOnNext { view.themeChanged() } - .takeUntil(view.activityResumedIntent.filter { resumed -> resumed }) - } - .autoDisposable(view.scope()) - .subscribe() + .filter { resumed -> !resumed } + .switchMap { + // Take until the activity is resumed + prefs.keyChanges + .filter { key -> key.contains("theme") } + .map { true } + .doOnNext { view.themeChanged() } + .takeUntil(view.activityResumedIntent.filter { resumed -> resumed }) + } + .autoDispose(view.scope()) + .subscribe() view.composeIntent - .autoDisposable(view.scope()) - .subscribe { navigator.showCompose() } + .autoDispose(view.scope()) + .subscribe { navigator.showCompose() } view.homeIntent - .withLatestFrom(state) { _, state -> - when { - state.page is Searching -> view.clearSearch() - state.page is Inbox && state.page.selected > 0 -> view.clearSelection() - state.page is Archived && state.page.selected > 0 -> view.clearSelection() + .withLatestFrom(state) { _, state -> + when { + state.page is Searching -> view.clearSearch() + state.page is Inbox && state.page.selected > 0 -> view.clearSelection() + state.page is Archived && state.page.selected > 0 -> view.clearSelection() - else -> newState { copy(drawerOpen = true) } - } + else -> newState { copy(drawerOpen = true) } } - .autoDisposable(view.scope()) - .subscribe() + } + .autoDispose(view.scope()) + .subscribe() view.drawerToggledIntent .doOnNext { newState { copy(drawerOpen = it) } view.drawerToggled(it) } - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe { open -> newState { copy(drawerOpen = open) } } view.navigationIntent - .withLatestFrom(state) { drawerItem, state -> - newState { copy(drawerOpen = false) } - when (drawerItem) { - NavItem.BACK -> when { - state.drawerOpen -> Unit - state.page is Searching -> view.clearSearch() - state.page is Inbox && state.page.selected > 0 -> view.clearSelection() - state.page is Archived && state.page.selected > 0 -> view.clearSelection() - state.page !is Inbox -> { - newState { - copy( - page = Inbox(data = inboxData(currentFilter)), - activeChip = if (currentFilter == UNREAD) MessageCategory.UNREAD else MessageCategory.ALL - ) - } + .withLatestFrom(state) { drawerItem, state -> + newState { copy(drawerOpen = false) } + when (drawerItem) { + NavItem.BACK -> when { + state.drawerOpen -> Unit + state.page is Searching -> view.clearSearch() + state.page is Inbox && state.page.selected > 0 -> view.clearSelection() + state.page is Archived && state.page.selected > 0 -> view.clearSelection() + state.page !is Inbox -> { + newState { + copy( + page = Inbox(data = inboxData(currentFilter)), + activeChip = if (currentFilter == UNREAD) MessageCategory.UNREAD else MessageCategory.ALL + ) } - else -> newState { copy(hasError = true) } } - NavItem.BACKUP -> navigator.showBackup() - NavItem.SCHEDULED -> navigator.showScheduled(null) - NavItem.BLOCKING -> navigator.showBlockedConversations() - NavItem.SETTINGS -> navigator.showSettings() -// NavItem.PLUS -> navigator.showQksmsPlusActivity("main_menu") -// NavItem.HELP -> navigator.showSupport() - NavItem.INVITE -> navigator.showInvite() - else -> Unit + + else -> newState { copy(hasError = true) } } - drawerItem + + NavItem.BACKUP -> navigator.showBackup() + NavItem.SCHEDULED -> navigator.showScheduled(null) + NavItem.BLOCKING -> navigator.showBlockedConversations() + NavItem.SETTINGS -> navigator.showSettings() + // NavItem.PLUS -> navigator.showQksmsPlusActivity("main_menu") + // NavItem.HELP -> navigator.showSupport() + NavItem.INVITE -> navigator.showInvite() + else -> Unit } - .distinctUntilChanged() - .doOnNext { drawerItem -> - when (drawerItem) { - NavItem.INBOX -> newState { - copy( - page = Inbox(data = inboxData(currentFilter)), - activeChip = if (currentFilter == UNREAD) MessageCategory.UNREAD else MessageCategory.ALL - ) - } + drawerItem + } + .distinctUntilChanged() + .doOnNext { drawerItem -> + when (drawerItem) { + NavItem.INBOX -> newState { + copy( + page = Inbox(data = inboxData(currentFilter)), + activeChip = if (currentFilter == UNREAD) MessageCategory.UNREAD else MessageCategory.ALL + ) + } - NavItem.ARCHIVED -> newState { - copy( - page = Archived(data = archivedData(ALL)), - currentFilter = ALL, - activeChip = MessageCategory.ARCHIVED - ) - } - else -> Unit + NavItem.ARCHIVED -> newState { + copy( + page = Archived(data = archivedData(ALL)), + currentFilter = ALL, + activeChip = MessageCategory.ARCHIVED + ) } + + else -> Unit } - .autoDisposable(view.scope()) - .subscribe() + } + .autoDispose(view.scope()) + .subscribe() view.optionsItemIntent .filter { itemId -> itemId == R.id.select_all } - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe { view.toggleSelectAll() } view.optionsItemIntent - .filter { itemId -> itemId == R.id.archive } - .withLatestFrom(view.conversationsSelectedIntent) { _, conversations -> - markArchived.execute(conversations) - lastArchivedThreadIds = conversations.toList() - view.showArchivedSnackbar(lastArchivedThreadIds.count(), true) - view.clearSelection() - } - .autoDisposable(view.scope()) - .subscribe() + .filter { itemId -> itemId == R.id.archive } + .withLatestFrom(view.conversationsSelectedIntent) { _, conversations -> + markArchived.execute(conversations) + lastArchivedThreadIds = conversations.toList() + view.showArchivedSnackbar(lastArchivedThreadIds.count(), true) + view.clearSelection() + } + .autoDispose(view.scope()) + .subscribe() view.optionsItemIntent - .filter { itemId -> itemId == R.id.unarchive } - .withLatestFrom(view.conversationsSelectedIntent) { _, conversations -> - markUnarchived.execute(conversations.toList()) - view.showArchivedSnackbar(conversations.count(), false) - view.clearSelection() - } - .autoDisposable(view.scope()) - .subscribe() + .filter { itemId -> itemId == R.id.unarchive } + .withLatestFrom(view.conversationsSelectedIntent) { _, conversations -> + markUnarchived.execute(conversations.toList()) + view.showArchivedSnackbar(conversations.count(), false) + view.clearSelection() + } + .autoDispose(view.scope()) + .subscribe() view.optionsItemIntent - .filter { itemId -> itemId == R.id.delete } - .filter { permissionManager.isDefaultSms().also { if (!it) view.requestDefaultSms() } } - .withLatestFrom(view.conversationsSelectedIntent) { _, conversations -> - view.showDeleteDialog(conversations) - } - .autoDisposable(view.scope()) - .subscribe() + .filter { itemId -> itemId == R.id.delete } + .filter { permissionManager.isDefaultSms().also { if (!it) view.requestDefaultSms() } } + .withLatestFrom(view.conversationsSelectedIntent) { _, conversations -> + view.showDeleteDialog(conversations) + } + .autoDispose(view.scope()) + .subscribe() view.optionsItemIntent - .filter { itemId -> itemId == R.id.add } - .withLatestFrom(view.conversationsSelectedIntent) { _, conversations -> conversations } - .doOnNext { view.clearSelection() } - .filter { conversations -> conversations.size == 1 } - .map { conversations -> conversations.first() } - .mapNotNull(conversationRepo::getConversation) - .map { conversation -> conversation.recipients } - .mapNotNull { recipients -> recipients[0]?.address?.takeIf { recipients.size == 1 } } - .doOnNext(navigator::addContact) - .autoDisposable(view.scope()) - .subscribe() + .filter { itemId -> itemId == R.id.add } + .withLatestFrom(view.conversationsSelectedIntent) { _, conversations -> conversations } + .doOnNext { view.clearSelection() } + .filter { conversations -> conversations.size == 1 } + .map { conversations -> conversations.first() } + .mapNotNull(conversationRepo::getConversation) + .map { conversation -> conversation.recipients } + .mapNotNull { recipients -> recipients[0]?.address?.takeIf { recipients.size == 1 } } + .doOnNext(navigator::addContact) + .autoDispose(view.scope()) + .subscribe() view.optionsItemIntent - .filter { itemId -> itemId == R.id.pin } - .withLatestFrom(view.conversationsSelectedIntent) { _, conversations -> - markPinned.execute(conversations.toList()) - view.clearSelection() - } - .autoDisposable(view.scope()) - .subscribe() + .filter { itemId -> itemId == R.id.pin } + .withLatestFrom(view.conversationsSelectedIntent) { _, conversations -> + markPinned.execute(conversations.toList()) + view.clearSelection() + } + .autoDispose(view.scope()) + .subscribe() view.optionsItemIntent - .filter { itemId -> itemId == R.id.unpin } - .withLatestFrom(view.conversationsSelectedIntent) { _, conversations -> - markUnpinned.execute(conversations.toList()) - view.clearSelection() - } - .autoDisposable(view.scope()) - .subscribe() + .filter { itemId -> itemId == R.id.unpin } + .withLatestFrom(view.conversationsSelectedIntent) { _, conversations -> + markUnpinned.execute(conversations.toList()) + view.clearSelection() + } + .autoDispose(view.scope()) + .subscribe() view.optionsItemIntent - .filter { itemId -> itemId == R.id.read } - .filter { permissionManager.isDefaultSms().also { if (!it) view.requestDefaultSms() } } - .withLatestFrom(view.conversationsSelectedIntent) { _, conversations -> - markRead.execute(conversations.toList()) - view.clearSelection() - } - .autoDisposable(view.scope()) - .subscribe() + .filter { itemId -> itemId == R.id.read } + .filter { permissionManager.isDefaultSms().also { if (!it) view.requestDefaultSms() } } + .withLatestFrom(view.conversationsSelectedIntent) { _, conversations -> + markRead.execute(conversations.toList()) + view.clearSelection() + } + .autoDispose(view.scope()) + .subscribe() view.optionsItemIntent - .filter { itemId -> itemId == R.id.unread } - .filter { permissionManager.isDefaultSms().also { if (!it) view.requestDefaultSms() } } - .withLatestFrom(view.conversationsSelectedIntent) { _, conversations -> - markUnread.execute(conversations.toList()) - view.clearSelection() - } - .autoDisposable(view.scope()) - .subscribe() + .filter { itemId -> itemId == R.id.unread } + .filter { permissionManager.isDefaultSms().also { if (!it) view.requestDefaultSms() } } + .withLatestFrom(view.conversationsSelectedIntent) { _, conversations -> + markUnread.execute(conversations.toList()) + view.clearSelection() + } + .autoDispose(view.scope()) + .subscribe() view.optionsItemIntent - .filter { itemId -> itemId == R.id.block } - .withLatestFrom(view.conversationsSelectedIntent) { _, conversations -> - view.showBlockingDialog(conversations.toList(), true) - view.clearSelection() - } - .autoDisposable(view.scope()) - .subscribe() + .filter { itemId -> itemId == R.id.block } + .withLatestFrom(view.conversationsSelectedIntent) { _, conversations -> + view.showBlockingDialog(conversations.toList(), true) + view.clearSelection() + } + .autoDispose(view.scope()) + .subscribe() view.optionsItemIntent .filter { itemId -> itemId == R.id.rename } .withLatestFrom(view.conversationsSelectedIntent) { _, conversationIds -> conversationIds.first() } .mapNotNull { conversationId -> conversationRepo.getConversation(conversationId) } - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe { conversation -> view.showRenameDialog(conversation.name) } // view.plusBannerIntent @@ -538,57 +542,67 @@ class MainViewModel @Inject constructor( // } view.rateIntent - .autoDisposable(view.scope()) - .subscribe { - navigator.showRating() - ratingManager.rate() - } + .autoDispose(view.scope()) + .subscribe { + navigator.showRating() + ratingManager.rate() + } view.dismissRatingIntent - .autoDisposable(view.scope()) - .subscribe { ratingManager.dismiss() } + .autoDispose(view.scope()) + .subscribe { ratingManager.dismiss() } view.conversationsSelectedIntent - .withLatestFrom(state) { selection, state -> - val conversations = selection.mapNotNull(conversationRepo::getConversation) - val add = conversations.firstOrNull() - ?.takeIf { conversations.size == 1 } - ?.takeIf { conversation -> conversation.recipients.size == 1 } - ?.recipients?.first() - ?.takeIf { recipient -> recipient.contact == null } != null - val pin = conversations.sumBy { if (it.pinned) -1 else 1 } >= 0 - val read = when (conversations.size) { - 0 -> false - 1 -> conversations[0].unread - else -> true + .withLatestFrom(state) { selection, state -> + val conversations = selection.mapNotNull(conversationRepo::getConversation) + val add = conversations.firstOrNull() + ?.takeIf { conversations.size == 1 } + ?.takeIf { conversation -> conversation.recipients.size == 1 } + ?.recipients?.first() + ?.takeIf { recipient -> recipient.contact == null } != null + val pin = conversations.sumBy { if (it.pinned) -1 else 1 } >= 0 + val read = when (conversations.size) { + 0 -> false + 1 -> conversations[0].unread + else -> true + } + val selected = selection.size + + when (state.page) { + is Inbox -> { + val page = state.page.copy( + addContact = add, + markPinned = pin, + markRead = read, + selected = selected + ) + newState { copy(page = page) } } - val selected = selection.size - - when (state.page) { - is Inbox -> { - val page = state.page.copy(addContact = add, markPinned = pin, markRead = read, selected = selected) - newState { copy(page = page) } - } - - is Archived -> { - val page = state.page.copy(addContact = add, markPinned = pin, markRead = read, selected = selected) - newState { copy(page = page) } - } - is Searching -> {} // Ignore - else -> {} + is Archived -> { + val page = state.page.copy( + addContact = add, + markPinned = pin, + markRead = read, + selected = selected + ) + newState { copy(page = page) } } + + is Searching -> {} // Ignore + else -> {} } - .autoDisposable(view.scope()) - .subscribe() + } + .autoDispose(view.scope()) + .subscribe() // Delete the conversation view.confirmDeleteIntent - .autoDisposable(view.scope()) - .subscribe { conversations -> - deleteConversations.execute(conversations.toList()) - view.clearSelection() - } + .autoDispose(view.scope()) + .subscribe { conversations -> + deleteConversations.execute(conversations.toList()) + view.clearSelection() + } view.renameConversationIntent .withLatestFrom(view.conversationsSelectedIntent) { newConversationName, selectedConversationIds -> @@ -604,64 +618,68 @@ class MainViewModel @Inject constructor( .observeOn(AndroidSchedulers.mainThread()) } .flatMapCompletable { it } - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe() view.swipeConversationIntent - .autoDisposable(view.scope()) - .subscribe { (threadId, direction) -> - val action = - if (direction == ItemTouchHelper.RIGHT) prefs.swipeRight.get() - else prefs.swipeLeft.get() - when (action) { - Preferences.SWIPE_ACTION_ARCHIVE -> - markArchived.execute(listOf(threadId)) { - lastArchivedThreadIds = listOf(threadId) - view.showArchivedSnackbar(1, true) - } - Preferences.SWIPE_ACTION_DELETE -> - view.showDeleteDialog(listOf(threadId)) - Preferences.SWIPE_ACTION_BLOCK -> - view.showBlockingDialog(listOf(threadId), true) - Preferences.SWIPE_ACTION_CALL -> { - ( + .autoDispose(view.scope()) + .subscribe { (threadId, direction) -> + val action = + if (direction == ItemTouchHelper.RIGHT) prefs.swipeRight.get() + else prefs.swipeLeft.get() + when (action) { + Preferences.SWIPE_ACTION_ARCHIVE -> + markArchived.execute(listOf(threadId)) { + lastArchivedThreadIds = listOf(threadId) + view.showArchivedSnackbar(1, true) + } + + Preferences.SWIPE_ACTION_DELETE -> + view.showDeleteDialog(listOf(threadId)) + + Preferences.SWIPE_ACTION_BLOCK -> + view.showBlockingDialog(listOf(threadId), true) + + Preferences.SWIPE_ACTION_CALL -> { + ( messageRepo.getMessagesSync(threadId).lastOrNull { !it.isMe() } ?.address // most recent non-me msg address - ?: conversationRepo.getConversation(threadId) - ?.recipients?.firstOrNull()?.address // first recipient in convo - )?.let(navigator::makePhoneCall) - } - Preferences.SWIPE_ACTION_READ -> markRead.execute(listOf(threadId)) - Preferences.SWIPE_ACTION_UNREAD -> markUnread.execute(listOf(threadId)) - Preferences.SWIPE_ACTION_SPEAK -> speakThreads.execute(listOf(threadId)) + ?: conversationRepo.getConversation(threadId) + ?.recipients?.firstOrNull()?.address // first recipient in convo + )?.let(navigator::makePhoneCall) } + + Preferences.SWIPE_ACTION_READ -> markRead.execute(listOf(threadId)) + Preferences.SWIPE_ACTION_UNREAD -> markUnread.execute(listOf(threadId)) + Preferences.SWIPE_ACTION_SPEAK -> speakThreads.execute(listOf(threadId)) } + } view.undoArchiveIntent - .autoDisposable(view.scope()) - .subscribe { - markUnarchived.execute(lastArchivedThreadIds.toList()) - lastArchivedThreadIds = listOf() - } + .autoDispose(view.scope()) + .subscribe { + markUnarchived.execute(lastArchivedThreadIds.toList()) + lastArchivedThreadIds = listOf() + } view.snackbarButtonIntent - .withLatestFrom(state) { _, state -> - when { - !state.defaultSms -> view.requestDefaultSms() - !state.smsPermission -> view.requestPermissions() - !state.contactPermission -> view.requestPermissions() - !state.notificationPermission -> { - if (prefs.hasAskedForNotificationPermission.get()) { - navigator.showPermissions() - } else { - prefs.hasAskedForNotificationPermission.set(true) - view.requestPermissions() - } + .withLatestFrom(state) { _, state -> + when { + !state.defaultSms -> view.requestDefaultSms() + !state.smsPermission -> view.requestPermissions() + !state.contactPermission -> view.requestPermissions() + !state.notificationPermission -> { + if (prefs.hasAskedForNotificationPermission.get()) { + navigator.showPermissions() + } else { + prefs.hasAskedForNotificationPermission.set(true) + view.requestPermissions() } } } - .autoDisposable(view.scope()) - .subscribe() + } + .autoDispose(view.scope()) + .subscribe() } } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/main/SearchAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/main/SearchAdapter.kt index 42eb7da9c..92485c85c 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/main/SearchAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/main/SearchAdapter.kt @@ -31,9 +31,9 @@ import org.prauga.messages.common.base.QkBindingViewHolder import org.prauga.messages.common.util.Colors import org.prauga.messages.common.util.DateFormatter import org.prauga.messages.common.util.extensions.setVisible +import org.prauga.messages.databinding.SearchListItemBinding import org.prauga.messages.extensions.removeAccents import org.prauga.messages.model.SearchResult -import org.prauga.messages.databinding.SearchListItemBinding import javax.inject.Inject class SearchAdapter @Inject constructor( @@ -45,17 +45,26 @@ class SearchAdapter @Inject constructor( private val highlightColor: Int by lazy { colors.theme().highlight } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QkBindingViewHolder { - val binding = SearchListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): QkBindingViewHolder { + val binding = + SearchListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) return QkBindingViewHolder(binding).apply { itemView.setOnClickListener { val result = getItem(adapterPosition) - navigator.showConversation(result.conversation.id, result.query.takeIf { result.messages > 0 }) + navigator.showConversation( + result.conversation.id, + result.query.takeIf { result.messages > 0 }) } } } - override fun onBindViewHolder(holder: QkBindingViewHolder, position: Int) { + override fun onBindViewHolder( + holder: QkBindingViewHolder, + position: Int + ) { val previous = data.getOrNull(position - 1) val result = getItem(position) @@ -66,7 +75,12 @@ class SearchAdapter @Inject constructor( var index = title.removeAccents().indexOf(query, ignoreCase = true) while (index >= 0) { - title.setSpan(BackgroundColorSpan(highlightColor), index, index + query.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + title.setSpan( + BackgroundColorSpan(highlightColor), + index, + index + query.length, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) index = title.indexOf(query, index + query.length, true) } holder.binding.title.text = title @@ -76,7 +90,8 @@ class SearchAdapter @Inject constructor( when (result.messages == 0) { true -> { holder.binding.date.setVisible(true) - holder.binding.date.text = dateFormatter.getConversationTimestamp(result.conversation.date) + holder.binding.date.text = + dateFormatter.getConversationTimestamp(result.conversation.date) holder.binding.snippet.text = when (result.conversation.me) { true -> context.getString(R.string.main_sender_you, result.conversation.snippet) false -> result.conversation.snippet @@ -85,7 +100,8 @@ class SearchAdapter @Inject constructor( false -> { holder.binding.date.setVisible(false) - holder.binding.snippet.text = context.getString(R.string.main_message_results, result.messages) + holder.binding.snippet.text = + context.getString(R.string.main_message_results, result.messages) } } } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/notificationprefs/NotificationPrefsActivity.kt b/presentation/src/main/java/com/moez/QKSMS/feature/notificationprefs/NotificationPrefsActivity.kt index b84c053d8..fd6604d95 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/notificationprefs/NotificationPrefsActivity.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/notificationprefs/NotificationPrefsActivity.kt @@ -28,10 +28,12 @@ import android.os.Bundle import androidx.core.view.isVisible import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProviders -import com.jakewharton.rxbinding2.view.clicks -import com.uber.autodispose.android.lifecycle.scope -import com.uber.autodispose.autoDisposable +import androidx.lifecycle.lifecycleScope import dagger.android.AndroidInjection +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.rx2.asFlow import org.prauga.messages.R import org.prauga.messages.common.QkDialog import org.prauga.messages.common.base.QkThemedActivity @@ -39,9 +41,6 @@ import org.prauga.messages.common.util.extensions.animateLayoutChanges import org.prauga.messages.common.util.extensions.setVisible import org.prauga.messages.common.widget.PreferenceView import org.prauga.messages.databinding.NotificationPrefsActivityBinding -import io.reactivex.Observable -import io.reactivex.subjects.PublishSubject -import io.reactivex.subjects.Subject import javax.inject.Inject class NotificationPrefsActivity : QkThemedActivity( @@ -57,10 +56,17 @@ class NotificationPrefsActivity : QkThemedActivity = PublishSubject.create() - override val previewModeSelectedIntent by lazy { previewModeDialog.adapter.menuItemClicks } - override val ringtoneSelectedIntent: Subject = PublishSubject.create() - override val actionsSelectedIntent by lazy { actionsDialog.adapter.menuItemClicks } + private val _preferenceClickIntent = MutableSharedFlow(extraBufferCapacity = 1) + private val _ringtoneSelectedIntent = MutableSharedFlow(extraBufferCapacity = 1) + + override val preferenceClickIntent: Flow = _preferenceClickIntent + override val previewModeSelectedIntent: Flow by lazy { + previewModeDialog.adapter.menuItemClicks.asFlow() + } + override val ringtoneSelectedIntent: Flow = _ringtoneSelectedIntent + override val actionsSelectedIntent: Flow by lazy { + actionsDialog.adapter.menuItemClicks.asFlow() + } private val viewModel by lazy { ViewModelProviders.of(this, viewModelFactory)[NotificationPrefsViewModel::class.java] @@ -90,10 +96,13 @@ class NotificationPrefsActivity : QkThemedActivity binding.preferences.getChildAt(index) } .mapNotNull { view -> view as? PreferenceView } - .map { preference -> preference.clicks().map { preference } } - .let { Observable.merge(it) } - .autoDisposable(scope()) - .subscribe(preferenceClickIntent) + .forEach { preference -> + preference.setOnClickListener { + lifecycleScope.launch { + _preferenceClickIntent.emit(preference) + } + } + } } override fun render(state: NotificationPrefsState) { @@ -147,8 +156,12 @@ class NotificationPrefsActivity : QkThemedActivity + * Copyright (C) 2025 Saalim Quadri * * This file is part of QKSMS. * @@ -16,20 +17,20 @@ * You should have received a copy of the GNU General Public License * along with QKSMS. If not, see . */ + package org.prauga.messages.feature.notificationprefs import android.net.Uri +import kotlinx.coroutines.flow.Flow import org.prauga.messages.common.base.QkView import org.prauga.messages.common.widget.PreferenceView -import io.reactivex.Observable -import io.reactivex.subjects.Subject interface NotificationPrefsView : QkView { - val preferenceClickIntent: Subject - val previewModeSelectedIntent: Subject - val ringtoneSelectedIntent: Observable - val actionsSelectedIntent: Subject + val preferenceClickIntent: Flow + val previewModeSelectedIntent: Flow + val ringtoneSelectedIntent: Flow + val actionsSelectedIntent: Flow fun showPreviewModeDialog() fun showRingtonePicker(default: Uri?) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/notificationprefs/NotificationPrefsViewModel.kt b/presentation/src/main/java/com/moez/QKSMS/feature/notificationprefs/NotificationPrefsViewModel.kt index 9f796671c..97b837b99 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/notificationprefs/NotificationPrefsViewModel.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/notificationprefs/NotificationPrefsViewModel.kt @@ -1,5 +1,6 @@ /* * Copyright (C) 2017 Moez Bhatti + * Copyright (C) 2025 Saalim Quadri * * This file is part of QKSMS. * @@ -16,22 +17,21 @@ * You should have received a copy of the GNU General Public License * along with QKSMS. If not, see . */ + package org.prauga.messages.feature.notificationprefs import android.content.Context import android.media.RingtoneManager import android.net.Uri -import com.uber.autodispose.android.lifecycle.scope -import com.uber.autodispose.autoDisposable +import androidx.lifecycle.viewModelScope +import com.moez.QKSMS.common.base.PvotViewModel +import com.moez.QKSMS.util.asFlow +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import org.prauga.messages.R import org.prauga.messages.common.Navigator -import org.prauga.messages.common.base.QkViewModel -import org.prauga.messages.extensions.mapNotNull import org.prauga.messages.repository.ConversationRepository import org.prauga.messages.util.Preferences -import io.reactivex.Flowable -import io.reactivex.rxkotlin.plusAssign -import io.reactivex.schedulers.Schedulers import javax.inject.Inject import javax.inject.Named @@ -41,7 +41,9 @@ class NotificationPrefsViewModel @Inject constructor( private val conversationRepo: ConversationRepository, private val navigator: Navigator, private val prefs: Preferences -) : QkViewModel(NotificationPrefsState(threadId = threadId)) { +) : PvotViewModel( + NotificationPrefsState(threadId = threadId) +) { private val notifications = prefs.notifications(threadId) private val previews = prefs.notificationPreviews(threadId) @@ -50,106 +52,170 @@ class NotificationPrefsViewModel @Inject constructor( private val ringtone = prefs.ringtone(threadId) init { - disposables += Flowable.just(threadId) - .mapNotNull { threadId -> conversationRepo.getConversation(threadId) } - .map { conversation -> conversation.getTitle() } - .subscribeOn(Schedulers.io()) - .subscribe { title -> newState { copy(conversationTitle = title) } } - - disposables += notifications.asObservable() - .subscribe { enabled -> newState { copy(notificationsEnabled = enabled) } } - - val previewLabels = context.resources.getStringArray(R.array.notification_preview_options) - disposables += previews.asObservable() - .subscribe { previewId -> - newState { copy(previewSummary = previewLabels[previewId], previewId = previewId) } + // title + viewModelScope.launch(Dispatchers.IO) { + val title = conversationRepo.getConversation(threadId)?.getTitle() + if (title != null) { + newState { copy(conversationTitle = title) } + } + } + + // notifications enabled + viewModelScope.launch { + notifications.asFlow().collect { enabled -> + newState { copy(notificationsEnabled = enabled) } + } + } + + val previewLabels = context.resources.getStringArray( + R.array.notification_preview_options + ) + + viewModelScope.launch { + previews.asFlow().collect { previewId -> + newState { + copy( + previewSummary = previewLabels[previewId], + previewId = previewId + ) } - - val actionLabels = context.resources.getStringArray(R.array.notification_actions) - disposables += prefs.notifAction1.asObservable() - .subscribe { previewId -> newState { copy(action1Summary = actionLabels[previewId]) } } - - disposables += prefs.notifAction2.asObservable() - .subscribe { previewId -> newState { copy(action2Summary = actionLabels[previewId]) } } - - disposables += prefs.notifAction3.asObservable() - .subscribe { previewId -> newState { copy(action3Summary = actionLabels[previewId]) } } - - disposables += wake.asObservable() - .subscribe { enabled -> newState { copy(wakeEnabled = enabled) } } - - disposables += prefs.silentNotContact.asObservable() - .subscribe { enabled -> newState { copy(silentNotContact = enabled) } } - - disposables += vibration.asObservable() - .subscribe { enabled -> newState { copy(vibrationEnabled = enabled) } } - - disposables += ringtone.asObservable() - .map { uriString -> - uriString.takeIf { it.isNotEmpty() } - ?.let(Uri::parse) - ?.let { uri -> RingtoneManager.getRingtone(context, uri) }?.getTitle(context) - ?: context.getString(R.string.settings_ringtone_none) - } - .subscribe { title -> newState { copy(ringtoneName = title) } } - - disposables += prefs.qkreply.asObservable() - .subscribe { enabled -> newState { copy(qkReplyEnabled = enabled) } } - - disposables += prefs.qkreplyTapDismiss.asObservable() - .subscribe { enabled -> newState { copy(qkReplyTapDismiss = enabled) } } + } + } + + val actionLabels = + context.resources.getStringArray(R.array.notification_actions) + + viewModelScope.launch { + prefs.notifAction1.asFlow().collect { id -> + newState { copy(action1Summary = actionLabels[id]) } + } + } + + viewModelScope.launch { + prefs.notifAction2.asFlow().collect { id -> + newState { copy(action2Summary = actionLabels[id]) } + } + } + + viewModelScope.launch { + prefs.notifAction3.asFlow().collect { id -> + newState { copy(action3Summary = actionLabels[id]) } + } + } + + viewModelScope.launch { + wake.asFlow().collect { enabled -> + newState { copy(wakeEnabled = enabled) } + } + } + + viewModelScope.launch { + prefs.silentNotContact.asFlow().collect { enabled -> + newState { copy(silentNotContact = enabled) } + } + } + + viewModelScope.launch { + vibration.asFlow().collect { enabled -> + newState { copy(vibrationEnabled = enabled) } + } + } + + viewModelScope.launch { + ringtone.asFlow().collect { uriString -> + val title = uriString + .takeIf { it.isNotEmpty() } + ?.let(Uri::parse) + ?.let { uri -> RingtoneManager.getRingtone(context, uri) } + ?.getTitle(context) + ?: context.getString(R.string.settings_ringtone_none) + + newState { copy(ringtoneName = title) } + } + } + + viewModelScope.launch { + prefs.qkreply.asFlow().collect { enabled -> + newState { copy(qkReplyEnabled = enabled) } + } + } + + viewModelScope.launch { + prefs.qkreplyTapDismiss.asFlow().collect { enabled -> + newState { copy(qkReplyTapDismiss = enabled) } + } + } } - override fun bindView(view: NotificationPrefsView) { + fun bindView(view: NotificationPrefsView) { super.bindView(view) - view.preferenceClickIntent - .autoDisposable(view.scope()) - .subscribe { - when (it.id) { - R.id.notificationsO -> navigator.showNotificationChannel(threadId) - - R.id.notifications -> notifications.set(!notifications.get()) - - R.id.previews -> view.showPreviewModeDialog() + var lastActionPreferenceId: Int? = null + viewModelScope.launch { + view.preferenceClickIntent.collect { pref -> + when (pref.id) { + R.id.notificationsO -> navigator.showNotificationChannel(threadId) - R.id.wake -> wake.set(!wake.get()) + R.id.notifications -> notifications.set(!notifications.get()) - R.id.silentNotContact -> prefs.silentNotContact.set(!prefs.silentNotContact.get()) + R.id.previews -> view.showPreviewModeDialog() - R.id.vibration -> vibration.set(!vibration.get()) + R.id.wake -> wake.set(!wake.get()) - R.id.ringtone -> view.showRingtonePicker(ringtone.get().takeIf { it.isNotEmpty() }?.let(Uri::parse)) + R.id.silentNotContact -> + prefs.silentNotContact.set(!prefs.silentNotContact.get()) - R.id.action1 -> view.showActionDialog(prefs.notifAction1.get()) + R.id.vibration -> vibration.set(!vibration.get()) - R.id.action2 -> view.showActionDialog(prefs.notifAction2.get()) - - R.id.action3 -> view.showActionDialog(prefs.notifAction3.get()) + R.id.ringtone -> view.showRingtonePicker( + ringtone.get() + .takeIf { it.isNotEmpty() } + ?.let(Uri::parse) + ) - R.id.qkreply -> prefs.qkreply.set(!prefs.qkreply.get()) + R.id.action1 -> { + lastActionPreferenceId = R.id.action1 + view.showActionDialog(prefs.notifAction1.get()) + } - R.id.qkreplyTapDismiss -> prefs.qkreplyTapDismiss.set(!prefs.qkreplyTapDismiss.get()) + R.id.action2 -> { + lastActionPreferenceId = R.id.action2 + view.showActionDialog(prefs.notifAction2.get()) } - } - view.previewModeSelectedIntent - .autoDisposable(view.scope()) - .subscribe { previews.set(it) } + R.id.action3 -> { + lastActionPreferenceId = R.id.action3 + view.showActionDialog(prefs.notifAction3.get()) + } - view.ringtoneSelectedIntent - .autoDisposable(view.scope()) - .subscribe { ringtone -> this.ringtone.set(ringtone) } + R.id.qkreply -> prefs.qkreply.set(!prefs.qkreply.get()) - view.actionsSelectedIntent - .withLatestFrom(view.preferenceClickIntent) { action, preference -> - when (preference.id) { - R.id.action1 -> prefs.notifAction1.set(action) - R.id.action2 -> prefs.notifAction2.set(action) - R.id.action3 -> prefs.notifAction3.set(action) - } + R.id.qkreplyTapDismiss -> + prefs.qkreplyTapDismiss.set(!prefs.qkreplyTapDismiss.get()) + } + } + } + + viewModelScope.launch { + view.previewModeSelectedIntent.collect { mode -> + previews.set(mode) + } + } + + viewModelScope.launch { + view.ringtoneSelectedIntent.collect { ringtone -> + this@NotificationPrefsViewModel.ringtone.set(ringtone) + } + } + + viewModelScope.launch { + view.actionsSelectedIntent.collect { action -> + when (lastActionPreferenceId) { + R.id.action1 -> prefs.notifAction1.set(action) + R.id.action2 -> prefs.notifAction2.set(action) + R.id.action3 -> prefs.notifAction3.set(action) } - .autoDisposable(view.scope()) - .subscribe() + } + } } } \ No newline at end of file diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/plus/PlusActivity.kt b/presentation/src/main/java/com/moez/QKSMS/feature/plus/PlusActivity.kt index 30683f61f..e0ecb5480 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/plus/PlusActivity.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/plus/PlusActivity.kt @@ -25,6 +25,9 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProviders import com.jakewharton.rxbinding2.view.clicks import dagger.android.AndroidInjection +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch import org.prauga.messages.R import org.prauga.messages.common.base.QkThemedActivity import org.prauga.messages.common.util.FontProvider @@ -34,22 +37,28 @@ import org.prauga.messages.common.util.extensions.setBackgroundTint import org.prauga.messages.common.util.extensions.setTint import org.prauga.messages.common.util.extensions.setVisible import org.prauga.messages.common.widget.PreferenceView +import org.prauga.messages.databinding.QksmsPlusActivityBinding import org.prauga.messages.feature.plus.experiment.UpgradeButtonExperiment import org.prauga.messages.manager.BillingManager -import org.prauga.messages.databinding.QksmsPlusActivityBinding -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject -class PlusActivity : QkThemedActivity(QksmsPlusActivityBinding::inflate), PlusView { - - @Inject lateinit var fontProvider: FontProvider - @Inject lateinit var upgradeButtonExperiment: UpgradeButtonExperiment - @Inject lateinit var viewModelFactory: ViewModelProvider.Factory - - private val viewModel by lazy { ViewModelProviders.of(this, viewModelFactory)[PlusViewModel::class.java] } +class PlusActivity : QkThemedActivity(QksmsPlusActivityBinding::inflate), + PlusView { + + @Inject + lateinit var fontProvider: FontProvider + @Inject + lateinit var upgradeButtonExperiment: UpgradeButtonExperiment + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + + private val viewModel by lazy { + ViewModelProviders.of( + this, + viewModelFactory + )[PlusViewModel::class.java] + } override val upgradeIntent get() = binding.upgrade.clicks() override val upgradeDonateIntent get() = binding.upgradeDonate.clicks() @@ -94,9 +103,12 @@ class PlusActivity : QkThemedActivity(QksmsPlusActivit } override fun render(state: PlusState) { - binding.description.text = getString(R.string.qksms_plus_description_summary, state.upgradePrice) - binding.upgrade.text = getString(upgradeButtonExperiment.variant, state.upgradePrice, state.currency) - binding.upgradeDonate.text = getString(R.string.qksms_plus_upgrade_donate, state.upgradeDonatePrice, state.currency) + binding.description.text = + getString(R.string.qksms_plus_description_summary, state.upgradePrice) + binding.upgrade.text = + getString(upgradeButtonExperiment.variant, state.upgradePrice, state.currency) + binding.upgradeDonate.text = + getString(R.string.qksms_plus_upgrade_donate, state.upgradeDonatePrice, state.currency) val fdroid = true diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/plus/PlusView.kt b/presentation/src/main/java/com/moez/QKSMS/feature/plus/PlusView.kt index 599d5b916..77f61f2ab 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/plus/PlusView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/plus/PlusView.kt @@ -18,9 +18,9 @@ */ package org.prauga.messages.feature.plus +import io.reactivex.Observable import org.prauga.messages.common.base.QkView import org.prauga.messages.manager.BillingManager -import io.reactivex.Observable interface PlusView : QkView { diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/plus/PlusViewModel.kt b/presentation/src/main/java/com/moez/QKSMS/feature/plus/PlusViewModel.kt index 0e0d104fd..d46f994c7 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/plus/PlusViewModel.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/plus/PlusViewModel.kt @@ -19,12 +19,12 @@ package org.prauga.messages.feature.plus import com.uber.autodispose.android.lifecycle.scope -import com.uber.autodispose.autoDisposable +import com.uber.autodispose.autoDispose +import io.reactivex.Observable +import io.reactivex.rxkotlin.plusAssign import org.prauga.messages.common.Navigator import org.prauga.messages.common.base.QkViewModel import org.prauga.messages.manager.BillingManager -import io.reactivex.Observable -import io.reactivex.rxkotlin.plusAssign import javax.inject.Inject class PlusViewModel @Inject constructor( @@ -34,51 +34,56 @@ class PlusViewModel @Inject constructor( init { disposables += billingManager.upgradeStatus - .subscribe { upgraded -> newState { copy(upgraded = upgraded) } } + .subscribe { upgraded -> newState { copy(upgraded = upgraded) } } disposables += billingManager.products - .subscribe { products -> - newState { - val upgrade = products.firstOrNull { it.sku == BillingManager.SKU_PLUS } - val upgradeDonate = products.firstOrNull { it.sku == BillingManager.SKU_PLUS_DONATE } - copy(upgradePrice = upgrade?.price ?: "", upgradeDonatePrice = upgradeDonate?.price ?: "", - currency = upgrade?.priceCurrencyCode ?: upgradeDonate?.priceCurrencyCode ?: "") - } + .subscribe { products -> + newState { + val upgrade = products.firstOrNull { it.sku == BillingManager.SKU_PLUS } + val upgradeDonate = + products.firstOrNull { it.sku == BillingManager.SKU_PLUS_DONATE } + copy( + upgradePrice = upgrade?.price ?: "", + upgradeDonatePrice = upgradeDonate?.price ?: "", + currency = upgrade?.priceCurrencyCode ?: upgradeDonate?.priceCurrencyCode + ?: "" + ) } + } } override fun bindView(view: PlusView) { super.bindView(view) Observable.merge( - view.upgradeIntent.map { BillingManager.SKU_PLUS }, - view.upgradeDonateIntent.map { BillingManager.SKU_PLUS_DONATE }) - .autoDisposable(view.scope()) - .subscribe { sku -> view.initiatePurchaseFlow(billingManager, sku) } + view.upgradeIntent.map { BillingManager.SKU_PLUS }, + view.upgradeDonateIntent.map { BillingManager.SKU_PLUS_DONATE }) + .autoDispose(view.scope()) + .subscribe { sku -> view.initiatePurchaseFlow(billingManager, sku) } // view.donateIntent // .autoDisposable(view.scope()) // .subscribe { navigator.showDonation() } view.themeClicks - .autoDisposable(view.scope()) - .subscribe { navigator.showSettings() } + .autoDispose(view.scope()) + .subscribe { navigator.showSettings() } view.scheduleClicks - .autoDisposable(view.scope()) - .subscribe { navigator.showScheduled(null) } + .autoDispose(view.scope()) + .subscribe { navigator.showScheduled(null) } view.backupClicks - .autoDisposable(view.scope()) - .subscribe { navigator.showBackup() } + .autoDispose(view.scope()) + .subscribe { navigator.showBackup() } view.delayedClicks - .autoDisposable(view.scope()) - .subscribe { navigator.showSettings() } + .autoDispose(view.scope()) + .subscribe { navigator.showSettings() } view.nightClicks - .autoDisposable(view.scope()) - .subscribe { navigator.showSettings() } + .autoDispose(view.scope()) + .subscribe { navigator.showSettings() } } } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/plus/experiment/UpgradeButtonExperiment.kt b/presentation/src/main/java/com/moez/QKSMS/feature/plus/experiment/UpgradeButtonExperiment.kt index 59217e036..1639dbaaa 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/plus/experiment/UpgradeButtonExperiment.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/plus/experiment/UpgradeButtonExperiment.kt @@ -31,10 +31,11 @@ class UpgradeButtonExperiment @Inject constructor( override val key: String = "Upgrade Button" override val variants: List> = listOf( - Variant("variant_a", R.string.qksms_plus_upgrade), - Variant("variant_b", R.string.qksms_plus_upgrade_b), - Variant("variant_c", R.string.qksms_plus_upgrade_c), - Variant("variant_d", R.string.qksms_plus_upgrade_d)) + Variant("variant_a", R.string.qksms_plus_upgrade), + Variant("variant_b", R.string.qksms_plus_upgrade_b), + Variant("variant_c", R.string.qksms_plus_upgrade_c), + Variant("variant_d", R.string.qksms_plus_upgrade_d) + ) override val default: Int = R.string.qksms_plus_upgrade diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/qkreply/QkReplyActivity.kt b/presentation/src/main/java/com/moez/QKSMS/feature/qkreply/QkReplyActivity.kt index 59109994b..2ea9bd052 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/qkreply/QkReplyActivity.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/qkreply/QkReplyActivity.kt @@ -39,6 +39,8 @@ import androidx.recyclerview.widget.RecyclerView import com.jakewharton.rxbinding2.view.clicks import com.jakewharton.rxbinding2.widget.textChanges import dagger.android.AndroidInjection +import io.reactivex.subjects.PublishSubject +import io.reactivex.subjects.Subject import org.prauga.messages.R import org.prauga.messages.common.base.QkThemedActivity import org.prauga.messages.common.util.extensions.autoScrollToStart @@ -47,8 +49,6 @@ import org.prauga.messages.common.util.extensions.showKeyboard import org.prauga.messages.common.widget.QkEditText import org.prauga.messages.databinding.QkreplyActivityBinding import org.prauga.messages.feature.compose.MessagesAdapter -import io.reactivex.subjects.PublishSubject -import io.reactivex.subjects.Subject import javax.inject.Inject class QkReplyActivity : QkThemedActivity(QkreplyActivityBinding::inflate), @@ -56,6 +56,7 @@ class QkReplyActivity : QkThemedActivity(QkreplyActivity @Inject lateinit var adapter: MessagesAdapter + @Inject lateinit var viewModelFactory: ViewModelProvider.Factory diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/qkreply/QkReplyActivityModule.kt b/presentation/src/main/java/com/moez/QKSMS/feature/qkreply/QkReplyActivityModule.kt index 4b71dc403..3157c1cdd 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/qkreply/QkReplyActivityModule.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/qkreply/QkReplyActivityModule.kt @@ -30,7 +30,8 @@ class QkReplyActivityModule { @Provides @Named("threadId") - fun provideThreadId(activity: QkReplyActivity): Long = activity.intent.extras?.getLong("threadId") ?: 0L + fun provideThreadId(activity: QkReplyActivity): Long = + activity.intent.extras?.getLong("threadId") ?: 0L @Provides @IntoMap diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/qkreply/QkReplyState.kt b/presentation/src/main/java/com/moez/QKSMS/feature/qkreply/QkReplyState.kt index 3bf1709e7..80da36fac 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/qkreply/QkReplyState.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/qkreply/QkReplyState.kt @@ -18,10 +18,10 @@ */ package org.prauga.messages.feature.qkreply +import io.realm.RealmResults import org.prauga.messages.compat.SubscriptionInfoCompat import org.prauga.messages.model.Conversation import org.prauga.messages.model.Message -import io.realm.RealmResults data class QkReplyState( val hasError: Boolean = false, diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/qkreply/QkReplyView.kt b/presentation/src/main/java/com/moez/QKSMS/feature/qkreply/QkReplyView.kt index 996bbc2d2..4806462a5 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/qkreply/QkReplyView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/qkreply/QkReplyView.kt @@ -18,8 +18,8 @@ */ package org.prauga.messages.feature.qkreply -import org.prauga.messages.common.base.QkView import io.reactivex.Observable +import org.prauga.messages.common.base.QkView interface QkReplyView : QkView { diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/qkreply/QkReplyViewModel.kt b/presentation/src/main/java/com/moez/QKSMS/feature/qkreply/QkReplyViewModel.kt index c88fce174..aa95a789c 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/qkreply/QkReplyViewModel.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/qkreply/QkReplyViewModel.kt @@ -20,7 +20,14 @@ package org.prauga.messages.feature.qkreply import android.telephony.SmsMessage import com.uber.autodispose.android.lifecycle.scope -import com.uber.autodispose.autoDisposable +import com.uber.autodispose.autoDispose +import io.reactivex.rxkotlin.Observables +import io.reactivex.rxkotlin.plusAssign +import io.reactivex.rxkotlin.withLatestFrom +import io.reactivex.schedulers.Schedulers +import io.reactivex.subjects.BehaviorSubject +import io.reactivex.subjects.Subject +import io.realm.RealmResults import org.prauga.messages.R import org.prauga.messages.common.Navigator import org.prauga.messages.common.base.QkViewModel @@ -34,13 +41,6 @@ import org.prauga.messages.model.Message import org.prauga.messages.repository.ConversationRepository import org.prauga.messages.repository.MessageRepository import org.prauga.messages.util.ActiveSubscriptionObservable -import io.reactivex.rxkotlin.Observables -import io.reactivex.rxkotlin.plusAssign -import io.reactivex.rxkotlin.withLatestFrom -import io.reactivex.schedulers.Schedulers -import io.reactivex.subjects.BehaviorSubject -import io.reactivex.subjects.Subject -import io.realm.RealmResults import java.util.concurrent.TimeUnit import javax.inject.Inject import javax.inject.Named @@ -58,14 +58,14 @@ class QkReplyViewModel @Inject constructor( private val conversation by lazy { conversationRepo.getConversationAsync(threadId) - .asObservable() - .filter { it.isLoaded } - .filter { it.isValid } - .distinctUntilChanged() + .asObservable() + .filter { it.isLoaded } + .filter { it.isValid } + .distinctUntilChanged() } private val messages: Subject> = - BehaviorSubject.createDefault(messageRepo.getUnreadMessages(threadId)) + BehaviorSubject.createDefault(messageRepo.getUnreadMessages(threadId)) init { disposables += markRead @@ -74,28 +74,29 @@ class QkReplyViewModel @Inject constructor( // When the set of messages changes, update the state // If we're ever showing an empty set of messages, then it's time to shut down to activity disposables += Observables - .combineLatest(messages, conversation) { messages, conversation -> - newState { copy(data = Pair(conversation, messages)) } - messages - } - .switchMap { messages -> messages.asObservable() } - .filter { it.isLoaded } - .filter { it.isValid } - .filter { it.isEmpty() } - .subscribe { newState { copy(hasError = true) } } + .combineLatest(messages, conversation) { messages, conversation -> + newState { copy(data = Pair(conversation, messages)) } + messages + } + .switchMap { messages -> messages.asObservable() } + .filter { it.isLoaded } + .filter { it.isValid } + .filter { it.isEmpty() } + .subscribe { newState { copy(hasError = true) } } disposables += conversation - .map { conversation -> conversation.getTitle() } - .distinctUntilChanged() - .subscribe { title -> newState { copy(title = title) } } + .map { conversation -> conversation.getTitle() } + .distinctUntilChanged() + .subscribe { title -> newState { copy(title = title) } } val latestSubId = messages - .map { messages -> messages.lastOrNull()?.subId ?: -1 } - .distinctUntilChanged() + .map { messages -> messages.lastOrNull()?.subId ?: -1 } + .distinctUntilChanged() val subscriptions = ActiveSubscriptionObservable(subscriptionManager) disposables += Observables.combineLatest(latestSubId, subscriptions) { subId, subs -> - val sub = if (subs.size > 1) subs.firstOrNull { it.subscriptionId == subId } ?: subs[0] else null + val sub = if (subs.size > 1) subs.firstOrNull { it.subscriptionId == subId } + ?: subs[0] else null newState { copy(subscription = sub) } }.subscribe() } @@ -107,16 +108,16 @@ class QkReplyViewModel @Inject constructor( .take(1) // only update saved draft to ui once .map { conversation -> conversation.draft } .distinctUntilChanged() - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe { draft -> view.setDraft(draft) } // Mark read view.menuItemIntent - .filter { id -> id == R.id.read } - .autoDisposable(view.scope()) - .subscribe { - markRead.execute(listOf(threadId)) { newState { copy(hasError = true) } } - } + .filter { id -> id == R.id.read } + .autoDispose(view.scope()) + .subscribe { + markRead.execute(listOf(threadId)) { newState { copy(hasError = true) } } + } // Call view.menuItemIntent @@ -127,104 +128,105 @@ class QkReplyViewModel @Inject constructor( ?: conversation.recipients.firstOrNull()?.address // first recipient in convo } .doOnNext { navigator.makePhoneCall(it) } - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe { newState { copy(hasError = true) } } // Show all messages view.menuItemIntent - .filter { id -> id == R.id.expand } - .map { messageRepo.getMessages(threadId) } - .doOnNext(messages::onNext) - .autoDisposable(view.scope()) - .subscribe { newState { copy(expanded = true) } } + .filter { id -> id == R.id.expand } + .map { messageRepo.getMessages(threadId) } + .doOnNext(messages::onNext) + .autoDispose(view.scope()) + .subscribe { newState { copy(expanded = true) } } // Show unread messages only view.menuItemIntent - .filter { id -> id == R.id.collapse } - .map { messageRepo.getUnreadMessages(threadId) } - .doOnNext(messages::onNext) - .autoDisposable(view.scope()) - .subscribe { newState { copy(expanded = false) } } + .filter { id -> id == R.id.collapse } + .map { messageRepo.getUnreadMessages(threadId) } + .doOnNext(messages::onNext) + .autoDispose(view.scope()) + .subscribe { newState { copy(expanded = false) } } // Delete new messages view.menuItemIntent - .filter { id -> id == R.id.delete } - .observeOn(Schedulers.io()) - .map { messageRepo.getUnreadMessages(threadId).map { it.id } } - .map { messages -> DeleteMessages.Params(messages, threadId) } - .autoDisposable(view.scope()) - .subscribe { deleteMessages.execute(it) { newState { copy(hasError = true) } } } + .filter { id -> id == R.id.delete } + .observeOn(Schedulers.io()) + .map { messageRepo.getUnreadMessages(threadId).map { it.id } } + .map { messages -> DeleteMessages.Params(messages, threadId) } + .autoDispose(view.scope()) + .subscribe { deleteMessages.execute(it) { newState { copy(hasError = true) } } } // View conversation view.menuItemIntent - .filter { id -> id == R.id.view } - .doOnNext { navigator.showConversation(threadId) } - .autoDisposable(view.scope()) - .subscribe { newState { copy(hasError = true) } } + .filter { id -> id == R.id.view } + .doOnNext { navigator.showConversation(threadId) } + .autoDispose(view.scope()) + .subscribe { newState { copy(hasError = true) } } // Enable the send button when there is text input into the new message body or there's // an attachment, disable otherwise view.textChangedIntent - .map { text -> text.isNotBlank() } - .autoDisposable(view.scope()) - .subscribe { canSend -> newState { copy(canSend = canSend) } } + .map { text -> text.isNotBlank() } + .autoDispose(view.scope()) + .subscribe { canSend -> newState { copy(canSend = canSend) } } // Show the remaining character counter when necessary view.textChangedIntent - .observeOn(Schedulers.computation()) - .map { draft -> SmsMessage.calculateLength(draft, false) } - .map { array -> - val messages = array[0] - val remaining = array[2] - - when { - messages <= 1 && remaining > 10 -> "" - messages <= 1 && remaining <= 10 -> "$remaining" - else -> "$remaining / $messages" - } + .observeOn(Schedulers.computation()) + .map { draft -> SmsMessage.calculateLength(draft, false) } + .map { array -> + val messages = array[0] + val remaining = array[2] + + when { + messages <= 1 && remaining > 10 -> "" + messages <= 1 && remaining <= 10 -> "$remaining" + else -> "$remaining / $messages" } - .distinctUntilChanged() - .autoDisposable(view.scope()) - .subscribe { remaining -> newState { copy(remaining = remaining) } } + } + .distinctUntilChanged() + .autoDispose(view.scope()) + .subscribe { remaining -> newState { copy(remaining = remaining) } } // Update the draft whenever the text is changed view.textChangedIntent - .debounce(100, TimeUnit.MILLISECONDS) - .map { draft -> draft.toString() } - .observeOn(Schedulers.io()) - .autoDisposable(view.scope()) - .subscribe { draft -> conversationRepo.saveDraft(threadId, draft) } + .debounce(100, TimeUnit.MILLISECONDS) + .map { draft -> draft.toString() } + .observeOn(Schedulers.io()) + .autoDispose(view.scope()) + .subscribe { draft -> conversationRepo.saveDraft(threadId, draft) } // Toggle to the next sim slot view.changeSimIntent - .withLatestFrom(state) { _, state -> - val subs = subscriptionManager.activeSubscriptionInfoList - val subIndex = subs.indexOfFirst { it.subscriptionId == state.subscription?.subscriptionId } - val subscription = when { - subIndex == -1 -> null - subIndex < subs.size - 1 -> subs[subIndex + 1] - else -> subs[0] - } - newState { copy(subscription = subscription) } + .withLatestFrom(state) { _, state -> + val subs = subscriptionManager.activeSubscriptionInfoList + val subIndex = + subs.indexOfFirst { it.subscriptionId == state.subscription?.subscriptionId } + val subscription = when { + subIndex == -1 -> null + subIndex < subs.size - 1 -> subs[subIndex + 1] + else -> subs[0] } - .autoDisposable(view.scope()) - .subscribe() + newState { copy(subscription = subscription) } + } + .autoDispose(view.scope()) + .subscribe() // Send a message when the send button is clicked, and disable editing mode if it's enabled view.sendIntent - .withLatestFrom(view.textChangedIntent) { _, body -> body } - .map { body -> body.toString() } - .withLatestFrom(state, conversation) { body, state, conversation -> - val subId = state.subscription?.subscriptionId ?: -1 - val addresses = conversation.recipients.map { it.address } - sendMessage.execute(SendMessage.Params(subId, threadId, addresses, body)) - view.setDraft("") - } - .doOnNext { - markRead.execute(listOf(threadId)) { newState { copy(hasError = true) } } - } - .autoDisposable(view.scope()) - .subscribe() + .withLatestFrom(view.textChangedIntent) { _, body -> body } + .map { body -> body.toString() } + .withLatestFrom(state, conversation) { body, state, conversation -> + val subId = state.subscription?.subscriptionId ?: -1 + val addresses = conversation.recipients.map { it.address } + sendMessage.execute(SendMessage.Params(subId, threadId, addresses, body)) + view.setDraft("") + } + .doOnNext { + markRead.execute(listOf(threadId)) { newState { copy(hasError = true) } } + } + .autoDispose(view.scope()) + .subscribe() } } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/scheduled/ScheduledViewModel.kt b/presentation/src/main/java/com/moez/QKSMS/feature/scheduled/ScheduledViewModel.kt index ef505c8bd..54e927e94 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/scheduled/ScheduledViewModel.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/scheduled/ScheduledViewModel.kt @@ -3,6 +3,7 @@ package org.prauga.messages.feature.scheduled import android.content.Context import com.uber.autodispose.android.lifecycle.scope import com.uber.autodispose.autoDisposable +import com.uber.autodispose.autoDispose import org.prauga.messages.R import org.prauga.messages.common.Navigator import org.prauga.messages.common.base.QkViewModel @@ -42,21 +43,22 @@ class ScheduledViewModel @Inject constructor( // update the state when the message selected count changes view.messagesSelectedIntent .map { selection -> selection.size } - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe { newState { copy(selectedMessages = it) } } // toggle select all / select none view.optionsItemIntent .filter { it == R.id.select_all } - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe { view.toggleSelectAll() } // show the delete message dialog if one or more messages selected view.optionsItemIntent .filter { it == R.id.delete } .withLatestFrom(view.messagesSelectedIntent) { _, selectedMessages -> - selectedMessages } - .autoDisposable(view.scope()) + selectedMessages + } + .autoDispose(view.scope()) .subscribe { val ids = it.mapNotNull(scheduledMessageRepo::getScheduledMessage) .map { it.id } @@ -68,8 +70,9 @@ class ScheduledViewModel @Inject constructor( view.optionsItemIntent .filter { it == R.id.copy } .withLatestFrom(view.messagesSelectedIntent) { _, selectedMessages -> - selectedMessages } - .autoDisposable(view.scope()) + selectedMessages + } + .autoDispose(view.scope()) .subscribe { val messages = it .mapNotNull(scheduledMessageRepo::getScheduledMessage) @@ -90,23 +93,25 @@ class ScheduledViewModel @Inject constructor( view.optionsItemIntent .filter { it == R.id.send_now } .withLatestFrom(view.messagesSelectedIntent) { _, selectedMessages -> - selectedMessages } - .autoDisposable(view.scope()) + selectedMessages + } + .autoDispose(view.scope()) .subscribe { view.showSendNowDialog(it) } // edit message menu item selected view.optionsItemIntent .filter { it == R.id.edit_message } .withLatestFrom(view.messagesSelectedIntent) { _, selectedMessage -> - selectedMessage.first() } - .autoDisposable(view.scope()) + selectedMessage.first() + } + .autoDispose(view.scope()) .subscribe { view.showEditMessageDialog(it) } // delete message(s) (fired after the confirmation dialog has been shown) view.deleteScheduledMessages .observeOn(AndroidSchedulers.mainThread()) .subscribeOn(Schedulers.io()) - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe { deleteScheduledMessagesInteractor.execute(it) view.clearSelection() @@ -116,7 +121,7 @@ class ScheduledViewModel @Inject constructor( view.sendScheduledMessages .observeOn(AndroidSchedulers.mainThread()) .subscribeOn(Schedulers.io()) - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe { it.forEach { sendScheduledMessageInteractor.execute(it) } view.clearSelection() @@ -127,7 +132,7 @@ class ScheduledViewModel @Inject constructor( view.editScheduledMessage .observeOn(AndroidSchedulers.mainThread()) .subscribeOn(Schedulers.io()) - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe { scheduledMessageRepo.getScheduledMessage(it)?.let { navigator.showCompose(it) @@ -142,7 +147,7 @@ class ScheduledViewModel @Inject constructor( .map { } .mergeWith(view.backPressedIntent) .withLatestFrom(state) { _, state -> state } - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe { when { (it.selectedMessages > 0) -> view.clearSelection() @@ -151,14 +156,14 @@ class ScheduledViewModel @Inject constructor( } view.composeIntent - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe { navigator.showCompose(mode = "scheduling") view.clearSelection() } view.upgradeIntent - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe { navigator.showQksmsPlusActivity("schedule_fab") } } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsController.kt b/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsController.kt index bff38e899..401c81f15 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsController.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/settings/SettingsController.kt @@ -58,6 +58,7 @@ import android.widget.CompoundButton import android.widget.LinearLayout import android.widget.ProgressBar import android.widget.ScrollView +import com.uber.autodispose.autoDispose import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext @@ -126,8 +127,8 @@ class SettingsController : QkController view.showThemePicker() + when (it.id) { + R.id.theme -> view.showThemePicker() - R.id.night -> view.showNightModeDialog() + R.id.night -> view.showNightModeDialog() - R.id.nightStart -> { - val date = nightModeManager.parseTime(prefs.nightStart.get()) - view.showStartTimePicker(date.get(Calendar.HOUR_OF_DAY), date.get(Calendar.MINUTE)) - } + R.id.nightStart -> { + val date = nightModeManager.parseTime(prefs.nightStart.get()) + view.showStartTimePicker( + date.get(Calendar.HOUR_OF_DAY), + date.get(Calendar.MINUTE) + ) + } - R.id.nightEnd -> { - val date = nightModeManager.parseTime(prefs.nightEnd.get()) - view.showEndTimePicker(date.get(Calendar.HOUR_OF_DAY), date.get(Calendar.MINUTE)) - } + R.id.nightEnd -> { + val date = nightModeManager.parseTime(prefs.nightEnd.get()) + view.showEndTimePicker( + date.get(Calendar.HOUR_OF_DAY), + date.get(Calendar.MINUTE) + ) + } - R.id.autoEmoji -> prefs.autoEmoji.set(!prefs.autoEmoji.get()) + R.id.autoEmoji -> prefs.autoEmoji.set(!prefs.autoEmoji.get()) - R.id.notifications -> navigator.showNotificationSettings() + R.id.notifications -> navigator.showNotificationSettings() - R.id.swipeActions -> view.showSwipeActions() + R.id.swipeActions -> view.showSwipeActions() - R.id.delayed -> view.showDelayDurationDialog() + R.id.delayed -> view.showDelayDurationDialog() - R.id.delivery -> prefs.delivery.set(!prefs.delivery.get()) + R.id.delivery -> prefs.delivery.set(!prefs.delivery.get()) - R.id.unreadAtTop -> prefs.unreadAtTop.set(!prefs.unreadAtTop.get()) + R.id.unreadAtTop -> prefs.unreadAtTop.set(!prefs.unreadAtTop.get()) - R.id.signature -> view.showSignatureDialog(prefs.signature.get()) + R.id.signature -> view.showSignatureDialog(prefs.signature.get()) - R.id.systemFont -> prefs.systemFont.set(!prefs.systemFont.get()) + R.id.systemFont -> prefs.systemFont.set(!prefs.systemFont.get()) - R.id.unicode -> prefs.unicode.set(!prefs.unicode.get()) + R.id.unicode -> prefs.unicode.set(!prefs.unicode.get()) - R.id.mobileOnly -> prefs.mobileOnly.set(!prefs.mobileOnly.get()) + R.id.mobileOnly -> prefs.mobileOnly.set(!prefs.mobileOnly.get()) - R.id.autoDelete -> view.showAutoDeleteDialog(prefs.autoDelete.get()) + R.id.autoDelete -> view.showAutoDeleteDialog(prefs.autoDelete.get()) - R.id.longAsMms -> prefs.longAsMms.set(!prefs.longAsMms.get()) + R.id.longAsMms -> prefs.longAsMms.set(!prefs.longAsMms.get()) - R.id.mmsSize -> view.showMmsSizePicker() + R.id.mmsSize -> view.showMmsSizePicker() - R.id.messsageLinkHandling -> view.showMessageLinkHandlingDialogPicker() + R.id.messsageLinkHandling -> view.showMessageLinkHandlingDialogPicker() - R.id.disableScreenshots -> prefs.disableScreenshots.set(!prefs.disableScreenshots.get()) + R.id.disableScreenshots -> prefs.disableScreenshots.set(!prefs.disableScreenshots.get()) - R.id.sync -> syncMessages.execute(Unit) + R.id.sync -> syncMessages.execute(Unit) - R.id.about -> view.showAbout() - } + R.id.about -> view.showAbout() } + } view.aboutLongClicks() - .map { !prefs.logging.get() } - .doOnNext { enabled -> prefs.logging.set(enabled) } - .autoDisposable(view.scope()) - .subscribe { enabled -> - context.makeToast(when (enabled) { + .map { !prefs.logging.get() } + .doOnNext { enabled -> prefs.logging.set(enabled) } + .autoDispose(view.scope()) + .subscribe { enabled -> + context.makeToast( + when (enabled) { true -> R.string.settings_logging_enabled false -> R.string.settings_logging_disabled - }) - } + } + ) + } view.nightModeSelected() - .withLatestFrom(billingManager.upgradeStatus) { mode, upgraded -> -// if (!upgraded && mode == Preferences.NIGHT_MODE_AUTO) { -// view.showQksmsPlusSnackbar() -// } else { - nightModeManager.updateNightMode(mode) -// } - } - .autoDisposable(view.scope()) - .subscribe() + .withLatestFrom(billingManager.upgradeStatus) { mode, upgraded -> + // if (!upgraded && mode == Preferences.NIGHT_MODE_AUTO) { + // view.showQksmsPlusSnackbar() + // } else { + nightModeManager.updateNightMode(mode) + // } + } + .autoDispose(view.scope()) + .subscribe() view.viewQksmsPlusClicks() - .autoDisposable(view.scope()) - .subscribe { navigator.showQksmsPlusActivity("settings_night") } + .autoDispose(view.scope()) + .subscribe { navigator.showQksmsPlusActivity("settings_night") } view.nightStartSelected() - .autoDisposable(view.scope()) - .subscribe { nightModeManager.setNightStart(it.first, it.second) } + .autoDispose(view.scope()) + .subscribe { nightModeManager.setNightStart(it.first, it.second) } view.nightEndSelected() - .autoDisposable(view.scope()) - .subscribe { nightModeManager.setNightEnd(it.first, it.second) } + .autoDispose(view.scope()) + .subscribe { nightModeManager.setNightEnd(it.first, it.second) } view.sendDelaySelected() - .withLatestFrom(billingManager.upgradeStatus) { duration, upgraded -> -// if (!upgraded && duration != 0) { -// view.showQksmsPlusSnackbar() -// } else { - prefs.sendDelay.set(duration) -// } - } - .autoDisposable(view.scope()) - .subscribe() + .withLatestFrom(billingManager.upgradeStatus) { duration, upgraded -> + // if (!upgraded && duration != 0) { + // view.showQksmsPlusSnackbar() + // } else { + prefs.sendDelay.set(duration) + // } + } + .autoDispose(view.scope()) + .subscribe() view.signatureChanged() - .doOnNext(prefs.signature::set) - .autoDisposable(view.scope()) - .subscribe() + .doOnNext(prefs.signature::set) + .autoDispose(view.scope()) + .subscribe() view.autoDeleteChanged() - .observeOn(Schedulers.io()) - .filter { maxAge -> - if (maxAge == 0) { - return@filter true - } - - val counts = messageRepo.getOldMessageCounts(maxAge) - if (counts.values.sum() == 0) { - return@filter true - } + .observeOn(Schedulers.io()) + .filter { maxAge -> + if (maxAge == 0) { + return@filter true + } - runBlocking { view.showAutoDeleteWarningDialog(counts.values.sum()) } + val counts = messageRepo.getOldMessageCounts(maxAge) + if (counts.values.sum() == 0) { + return@filter true } - .doOnNext { maxAge -> - when (maxAge == 0) { - true -> AutoDeleteService.cancelJob(context) - false -> { - AutoDeleteService.scheduleJob(context) - deleteOldMessages.execute(Unit) - } + + runBlocking { view.showAutoDeleteWarningDialog(counts.values.sum()) } + } + .doOnNext { maxAge -> + when (maxAge == 0) { + true -> AutoDeleteService.cancelJob(context) + false -> { + AutoDeleteService.scheduleJob(context) + deleteOldMessages.execute(Unit) } } - .doOnNext(prefs.autoDelete::set) - .autoDisposable(view.scope()) - .subscribe() + } + .doOnNext(prefs.autoDelete::set) + .autoDispose(view.scope()) + .subscribe() view.mmsSizeSelected() - .autoDisposable(view.scope()) - .subscribe(prefs.mmsSize::set) + .autoDispose(view.scope()) + .subscribe(prefs.mmsSize::set) view.messageLinkHandlingSelected() - .autoDisposable(view.scope()) + .autoDispose(view.scope()) .subscribe(prefs.messageLinkHandling::set) } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/settings/about/AboutPresenter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/settings/about/AboutPresenter.kt index c412eaacc..869913d73 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/settings/about/AboutPresenter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/settings/about/AboutPresenter.kt @@ -20,6 +20,7 @@ package org.prauga.messages.feature.settings.about import com.uber.autodispose.android.lifecycle.scope import com.uber.autodispose.autoDisposable +import com.uber.autodispose.autoDispose import org.prauga.messages.R import org.prauga.messages.common.Navigator import org.prauga.messages.common.base.QkPresenter @@ -33,20 +34,20 @@ class AboutPresenter @Inject constructor( super.bindIntents(view) view.preferenceClicks() - .autoDisposable(view.scope()) - .subscribe { preference -> - when (preference.id) { - R.id.developer -> navigator.showDeveloper() + .autoDispose(view.scope()) + .subscribe { preference -> + when (preference.id) { + R.id.developer -> navigator.showDeveloper() - R.id.source -> navigator.showSourceCode() + R.id.source -> navigator.showSourceCode() - R.id.changelog -> navigator.showChangelog() + R.id.changelog -> navigator.showChangelog() - R.id.contact -> navigator.showSupport() + R.id.contact -> navigator.showSupport() - R.id.license -> navigator.showLicense() - } + R.id.license -> navigator.showLicense() } + } } } \ No newline at end of file diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/settings/swipe/SwipeActionsController.kt b/presentation/src/main/java/com/moez/QKSMS/feature/settings/swipe/SwipeActionsController.kt index 7995c3ac3..dbc520582 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/settings/swipe/SwipeActionsController.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/settings/swipe/SwipeActionsController.kt @@ -37,6 +37,7 @@ import io.reactivex.subjects.PublishSubject import io.reactivex.subjects.Subject import android.widget.ImageView import android.widget.TextView +import com.uber.autodispose.autoDispose import javax.inject.Inject class SwipeActionsController : QkController(), SwipeActionsView { @@ -87,10 +88,10 @@ class SwipeActionsController : QkController - when (action) { - SwipeActionsView.Action.RIGHT -> prefs.swipeRight.get() - SwipeActionsView.Action.LEFT -> prefs.swipeLeft.get() - } + .map { action -> + when (action) { + SwipeActionsView.Action.RIGHT -> prefs.swipeRight.get() + SwipeActionsView.Action.LEFT -> prefs.swipeLeft.get() } - .autoDisposable(view.scope()) - .subscribe(view::showSwipeActions) + } + .autoDispose(view.scope()) + .subscribe(view::showSwipeActions) view.actionSelected() - .withLatestFrom(view.actionClicks()) { actionId, action -> - when (action) { - SwipeActionsView.Action.RIGHT -> prefs.swipeRight.set(actionId) - SwipeActionsView.Action.LEFT -> prefs.swipeLeft.set(actionId) - } + .withLatestFrom(view.actionClicks()) { actionId, action -> + when (action) { + SwipeActionsView.Action.RIGHT -> prefs.swipeRight.set(actionId) + SwipeActionsView.Action.LEFT -> prefs.swipeLeft.set(actionId) } - .autoDisposable(view.scope()) - .subscribe() + } + .autoDispose(view.scope()) + .subscribe() } @DrawableRes diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/themepicker/ThemePickerPresenter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/themepicker/ThemePickerPresenter.kt index 698431a04..7e08dfe1e 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/themepicker/ThemePickerPresenter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/themepicker/ThemePickerPresenter.kt @@ -21,6 +21,7 @@ package org.prauga.messages.feature.themepicker import com.f2prateek.rx.preferences2.Preference import com.uber.autodispose.android.lifecycle.scope import com.uber.autodispose.autoDisposable +import com.uber.autodispose.autoDispose import org.prauga.messages.common.Navigator import org.prauga.messages.common.base.QkPresenter import org.prauga.messages.common.util.Colors @@ -46,58 +47,61 @@ class ThemePickerPresenter @Inject constructor( super.bindIntents(view) theme.asObservable() - .autoDisposable(view.scope()) - .subscribe { color -> view.setCurrentTheme(color) } + .autoDispose(view.scope()) + .subscribe { color -> view.setCurrentTheme(color) } // Update the theme when a material theme is clicked view.themeSelected() - .autoDisposable(view.scope()) - .subscribe { color -> - theme.set(color) - if (recipientId == 0L) { - widgetManager.updateTheme() - } + .autoDispose(view.scope()) + .subscribe { color -> + theme.set(color) + if (recipientId == 0L) { + widgetManager.updateTheme() } + } // Update the color of the apply button view.hsvThemeSelected() - .doOnNext { color -> newState { copy(newColor = color) } } - .map { color -> colors.textPrimaryOnThemeForColor(color) } - .doOnNext { color -> newState { copy(newTextColor = color) } } - .autoDisposable(view.scope()) - .subscribe() + .doOnNext { color -> newState { copy(newColor = color) } } + .map { color -> colors.textPrimaryOnThemeForColor(color) } + .doOnNext { color -> newState { copy(newTextColor = color) } } + .autoDispose(view.scope()) + .subscribe() // Toggle the visibility of the apply group - Observables.combineLatest(theme.asObservable(), view.hsvThemeSelected()) { old, new -> old != new } - .autoDisposable(view.scope()) - .subscribe { themeChanged -> newState { copy(applyThemeVisible = themeChanged) } } + Observables.combineLatest( + theme.asObservable(), + view.hsvThemeSelected() + ) { old, new -> old != new } + .autoDispose(view.scope()) + .subscribe { themeChanged -> newState { copy(applyThemeVisible = themeChanged) } } // Update the theme, when apply is clicked view.applyHsvThemeClicks() - .withLatestFrom(view.hsvThemeSelected()) { _, color -> color } - .withLatestFrom(billingManager.upgradeStatus) { color, upgraded -> - if (!upgraded) { - view.showQksmsPlusSnackbar() - } else { - theme.set(color) - if (recipientId == 0L) { - widgetManager.updateTheme() - } + .withLatestFrom(view.hsvThemeSelected()) { _, color -> color } + .withLatestFrom(billingManager.upgradeStatus) { color, upgraded -> + if (!upgraded) { + view.showQksmsPlusSnackbar() + } else { + theme.set(color) + if (recipientId == 0L) { + widgetManager.updateTheme() } } - .autoDisposable(view.scope()) - .subscribe() + } + .autoDispose(view.scope()) + .subscribe() // Show QKSMS+ activity view.viewQksmsPlusClicks() - .autoDisposable(view.scope()) - .subscribe { navigator.showQksmsPlusActivity("settings_theme") } + .autoDispose(view.scope()) + .subscribe { navigator.showQksmsPlusActivity("settings_theme") } // Reset the theme view.clearHsvThemeClicks() - .withLatestFrom(theme.asObservable()) { _, color -> color } - .autoDisposable(view.scope()) - .subscribe { color -> view.setCurrentTheme(color) } + .withLatestFrom(theme.asObservable()) { _, color -> color } + .autoDispose(view.scope()) + .subscribe { color -> view.setCurrentTheme(color) } } } \ No newline at end of file diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/widget/WidgetAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/widget/WidgetAdapter.kt index 01742adc3..af3ea6326 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/widget/WidgetAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/widget/WidgetAdapter.kt @@ -50,14 +50,21 @@ class WidgetAdapter(intent: Intent) : RemoteViewsService.RemoteViewsFactory { private const val MAX_CONVERSATIONS_COUNT = 25 } - @Inject lateinit var context: Context - @Inject lateinit var colors: Colors - @Inject lateinit var conversationRepo: ConversationRepository - @Inject lateinit var dateFormatter: DateFormatter - @Inject lateinit var prefs: Preferences - - private val appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, - AppWidgetManager.INVALID_APPWIDGET_ID) + @Inject + lateinit var context: Context + @Inject + lateinit var colors: Colors + @Inject + lateinit var conversationRepo: ConversationRepository + @Inject + lateinit var dateFormatter: DateFormatter + @Inject + lateinit var prefs: Preferences + + private val appWidgetId = intent.getIntExtra( + AppWidgetManager.EXTRA_APPWIDGET_ID, + AppWidgetManager.INVALID_APPWIDGET_ID + ) private val smallWidget = intent.getBooleanExtra("small_widget", false) private var conversations: List = listOf() private val appWidgetManager by lazy { AppWidgetManager.getInstance(context) } @@ -114,7 +121,11 @@ class WidgetAdapter(intent: Intent) : RemoteViewsService.RemoteViewsFactory { remoteViews.setInt(R.id.avatarMask, "setColorFilter", background) val contact = conversation.recipients.map { recipient -> - recipient.contact ?: Contact().apply { numbers.add(PhoneNumber().apply { address = recipient.address }) } + recipient.contact ?: Contact().apply { + numbers.add(PhoneNumber().apply { + address = recipient.address + }) + } }.firstOrNull() // Use the icon if there's no name, otherwise show an initial @@ -128,9 +139,9 @@ class WidgetAdapter(intent: Intent) : RemoteViewsService.RemoteViewsFactory { remoteViews.setImageViewBitmap(R.id.photo, null) val futureGet = GlideApp.with(context) - .asBitmap() - .load(contact?.photoUri) - .submit(48.dpToPx(context), 48.dpToPx(context)) + .asBitmap() + .load(contact?.photoUri) + .submit(48.dpToPx(context), 48.dpToPx(context)) tryOrNull(false) { remoteViews.setImageViewBitmap(R.id.photo, futureGet.get()) } // Name @@ -140,7 +151,8 @@ class WidgetAdapter(intent: Intent) : RemoteViewsService.RemoteViewsFactory { }, conversation.unread)) // Date - val timestamp = conversation.date.takeIf { it > 0 }?.let(dateFormatter::getConversationTimestamp) + val timestamp = + conversation.date.takeIf { it > 0 }?.let(dateFormatter::getConversationTimestamp) remoteViews.setTextColor(R.id.date, if (conversation.unread) textPrimary else textTertiary) remoteViews.setTextViewText(R.id.date, boldText(timestamp, conversation.unread)) @@ -154,9 +166,15 @@ class WidgetAdapter(intent: Intent) : RemoteViewsService.RemoteViewsFactory { conversation.me -> context.getString(R.string.main_sender_you, conversation.snippet) else -> conversation.snippet } - remoteViews.setTextColor(R.id.snippet, if (conversation.unread) textPrimary else textTertiary) + remoteViews.setTextColor( + R.id.snippet, + if (conversation.unread) textPrimary else textTertiary + ) remoteViews.setTextViewText(R.id.snippet, boldText(snippet, conversation.unread)) - remoteViews.setTextViewText(R.id.snippet, italicText(snippet, conversation.draft.isNotEmpty())) + remoteViews.setTextViewText( + R.id.snippet, + italicText(snippet, conversation.draft.isNotEmpty()) + ) // set fill-in intent to be used for current item remoteViews.setOnClickFillInIntent( @@ -183,13 +201,15 @@ class WidgetAdapter(intent: Intent) : RemoteViewsService.RemoteViewsFactory { private fun boldText(text: CharSequence?, shouldBold: Boolean): CharSequence? = when { shouldBold -> SpannableStringBuilder() - .bold { append(text) } + .bold { append(text) } + else -> text } private fun italicText(text: CharSequence?, shouldBold: Boolean): CharSequence? = when { shouldBold -> SpannableStringBuilder() - .italic { append(text) } + .italic { append(text) } + else -> text } diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/widget/WidgetProvider.kt b/presentation/src/main/java/com/moez/QKSMS/feature/widget/WidgetProvider.kt index 8b2d2ecb5..65f97d744 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/widget/WidgetProvider.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/widget/WidgetProvider.kt @@ -28,6 +28,7 @@ import android.content.Intent import android.content.res.Configuration import android.os.Bundle import android.widget.RemoteViews +import androidx.core.net.toUri import dagger.android.AndroidInjection import org.prauga.messages.R import org.prauga.messages.common.util.Colors @@ -39,12 +40,13 @@ import org.prauga.messages.receiver.StartActivityFromWidgetReceiver import org.prauga.messages.util.Preferences import timber.log.Timber import javax.inject.Inject -import androidx.core.net.toUri class WidgetProvider : AppWidgetProvider() { - @Inject lateinit var colors: Colors - @Inject lateinit var prefs: Preferences + @Inject + lateinit var colors: Colors + @Inject + lateinit var prefs: Preferences override fun onReceive(context: Context, intent: Intent) { AndroidInjection.inject(this, context) @@ -58,7 +60,11 @@ class WidgetProvider : AppWidgetProvider() { /** * Update all widgets in the list */ - override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { + override fun onUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray + ) { super.onUpdate(context, appWidgetManager, appWidgetIds) updateData(context) @@ -72,7 +78,8 @@ class WidgetProvider : AppWidgetProvider() { */ private fun updateData(context: Context) { val appWidgetManager = AppWidgetManager.getInstance(context) - val appWidgetIds = appWidgetManager.getAppWidgetIds(ComponentName(context, WidgetProvider::class.java)) + val appWidgetIds = + appWidgetManager.getAppWidgetIds(ComponentName(context, WidgetProvider::class.java)) // We need to update all Mms appwidgets on the home screen. appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.conversations) @@ -81,7 +88,12 @@ class WidgetProvider : AppWidgetProvider() { /** * Update widget when widget size changes */ - override fun onAppWidgetOptionsChanged(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int, newOptions: Bundle) { + override fun onAppWidgetOptionsChanged( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetId: Int, + newOptions: Bundle + ) { updateWidget(context, appWidgetId, isSmallWidget(appWidgetManager, appWidgetId)) super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions) } @@ -110,25 +122,38 @@ class WidgetProvider : AppWidgetProvider() { Timber.v("updateWidget appWidgetId: $appWidgetId") val remoteViews = RemoteViews(context.packageName, R.layout.widget) - val nightModeFlags = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK + val nightModeFlags = + context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK val isNightMode = nightModeFlags == Configuration.UI_MODE_NIGHT_YES // Apply colors from theme val night = prefs.night.get() || isNightMode - remoteViews.setInt(R.id.background, "setColorFilter", context.getColorCompat(if (night) R.color.backgroundDark else R.color.white)) + remoteViews.setInt( + R.id.background, + "setColorFilter", + context.getColorCompat(if (night) R.color.backgroundDark else R.color.white) + ) - remoteViews.setInt(R.id.toolbar, "setColorFilter", context.getColorCompat(if (night) R.color.backgroundDark else R.color.backgroundLight)) + remoteViews.setInt( + R.id.toolbar, + "setColorFilter", + context.getColorCompat(if (night) R.color.backgroundDark else R.color.backgroundLight) + ) - remoteViews.setTextColor(R.id.title, context.getColorCompat(when (night) { - true -> R.color.textPrimaryDark - false -> R.color.textPrimary - })) + remoteViews.setTextColor( + R.id.title, context.getColorCompat( + when (night) { + true -> R.color.textPrimaryDark + false -> R.color.textPrimary + } + ) + ) // Set adapter for conversations val intent = Intent(context, WidgetService::class.java) - .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) - .putExtra("small_widget", smallWidget) + .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) + .putExtra("small_widget", smallWidget) intent.data = intent.toUri(Intent.URI_INTENT_SCHEME).toUri() remoteViews.setRemoteAdapter(R.id.conversations, intent) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/widget/WidgetSpeakUnseenProvider.kt b/presentation/src/main/java/com/moez/QKSMS/feature/widget/WidgetSpeakUnseenProvider.kt index e6a24f71c..5cfc047a5 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/widget/WidgetSpeakUnseenProvider.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/widget/WidgetSpeakUnseenProvider.kt @@ -34,7 +34,11 @@ class WidgetSpeakUnseenProvider : AppWidgetProvider() { super.onReceive(context, intent) } - override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { + override fun onUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray + ) { super.onUpdate(context, appWidgetManager, appWidgetIds) for (appWidgetId in appWidgetIds) @@ -54,8 +58,10 @@ class WidgetSpeakUnseenProvider : AppWidgetProvider() { // speak unseen intent val speakUnseenIntent = Intent(context, SpeakThreadsReceiver::class.java) .putExtra("threadId", -1L) - val speakUnseenPendingIntent = PendingIntent.getBroadcast(context,0, - speakUnseenIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + val speakUnseenPendingIntent = PendingIntent.getBroadcast( + context, 0, + speakUnseenIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) remoteViews.setOnClickPendingIntent(R.id.speakUnseenImage, speakUnseenPendingIntent) AppWidgetManager.getInstance(context).updateAppWidget(appWidgetId, remoteViews) diff --git a/presentation/src/main/java/com/moez/QKSMS/util/PreferenceExtensions.kt b/presentation/src/main/java/com/moez/QKSMS/util/PreferenceExtensions.kt new file mode 100644 index 000000000..e0c4e3b30 --- /dev/null +++ b/presentation/src/main/java/com/moez/QKSMS/util/PreferenceExtensions.kt @@ -0,0 +1,8 @@ +package com.moez.QKSMS.util + +import com.f2prateek.rx.preferences2.Preference +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.rx2.asFlow + +fun Preference.asFlow(): Flow = + this.asObservable().asFlow() \ No newline at end of file diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index cb56c92a8..6e98c4625 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -259,6 +259,11 @@ Schedule a message Schedule this message Scheduled message + + Send now + Copy text + Delete + Missing Appearance