Merge pull request #1504 from oxen-io/release-1.18.4
Merge release-1.18.4 to masterpull/1537/head 1.18.4
commit
b544961d28
@ -1,71 +1,131 @@
|
||||
package org.thoughtcrime.securesms.home
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.asFlow
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.cash.copper.flow.observeQuery
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.database.DatabaseContentProviders
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||
import java.lang.ref.WeakReference
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.observeChanges
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext as ApplicationContextQualifier
|
||||
|
||||
@HiltViewModel
|
||||
class HomeViewModel @Inject constructor(private val threadDb: ThreadDatabase): ViewModel() {
|
||||
class HomeViewModel @Inject constructor(
|
||||
private val threadDb: ThreadDatabase,
|
||||
private val contentResolver: ContentResolver,
|
||||
private val prefs: TextSecurePreferences,
|
||||
@ApplicationContextQualifier private val context: Context,
|
||||
) : ViewModel() {
|
||||
// SharedFlow that emits whenever the user asks us to reload the conversation
|
||||
private val manualReloadTrigger = MutableSharedFlow<Unit>(
|
||||
extraBufferCapacity = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
)
|
||||
|
||||
private val executor = viewModelScope + SupervisorJob()
|
||||
private var lastContext: WeakReference<Context>? = null
|
||||
private var updateJobs: MutableList<Job> = mutableListOf()
|
||||
/**
|
||||
* A [StateFlow] that emits the list of threads and the typing status of each thread.
|
||||
*
|
||||
* This flow will emit whenever the user asks us to reload the conversation list or
|
||||
* whenever the conversation list changes.
|
||||
*/
|
||||
val data: StateFlow<Data?> = combine(
|
||||
observeConversationList(),
|
||||
observeTypingStatus(),
|
||||
messageRequests(),
|
||||
::Data
|
||||
)
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, null)
|
||||
|
||||
private val _conversations = MutableLiveData<List<ThreadRecord>>()
|
||||
val conversations: LiveData<List<ThreadRecord>> = _conversations
|
||||
private fun hasHiddenMessageRequests() = TextSecurePreferences.events
|
||||
.filter { it == TextSecurePreferences.HAS_HIDDEN_MESSAGE_REQUESTS }
|
||||
.flowOn(Dispatchers.IO)
|
||||
.map { prefs.hasHiddenMessageRequests() }
|
||||
.onStart { emit(prefs.hasHiddenMessageRequests()) }
|
||||
|
||||
private val listUpdateChannel = Channel<Unit>(capacity = Channel.CONFLATED)
|
||||
private fun observeTypingStatus(): Flow<Set<Long>> =
|
||||
ApplicationContext.getInstance(context).typingStatusRepository
|
||||
.typingThreads
|
||||
.asFlow()
|
||||
.onStart { emit(emptySet()) }
|
||||
.distinctUntilChanged()
|
||||
|
||||
fun tryUpdateChannel() = listUpdateChannel.trySend(Unit)
|
||||
private fun messageRequests() = combine(
|
||||
unapprovedConversationCount(),
|
||||
hasHiddenMessageRequests(),
|
||||
latestUnapprovedConversationTimestamp(),
|
||||
::createMessageRequests
|
||||
)
|
||||
|
||||
fun getObservable(context: Context): LiveData<List<ThreadRecord>> {
|
||||
// If the context has changed (eg. the activity gets recreated) then
|
||||
// we need to cancel the old executors and recreate them to prevent
|
||||
// the app from triggering extra updates when data changes
|
||||
if (context != lastContext?.get()) {
|
||||
lastContext = WeakReference(context)
|
||||
updateJobs.forEach { it.cancel() }
|
||||
updateJobs.clear()
|
||||
private fun unapprovedConversationCount() = reloadTriggersAndContentChanges()
|
||||
.map { threadDb.unapprovedConversationCount }
|
||||
|
||||
updateJobs.add(
|
||||
executor.launch(Dispatchers.IO) {
|
||||
context.contentResolver
|
||||
.observeQuery(DatabaseContentProviders.ConversationList.CONTENT_URI)
|
||||
.onEach { listUpdateChannel.trySend(Unit) }
|
||||
.collect()
|
||||
}
|
||||
)
|
||||
updateJobs.add(
|
||||
executor.launch(Dispatchers.IO) {
|
||||
for (update in listUpdateChannel) {
|
||||
threadDb.approvedConversationList.use { openCursor ->
|
||||
val reader = threadDb.readerFor(openCursor)
|
||||
val threads = mutableListOf<ThreadRecord>()
|
||||
while (true) {
|
||||
threads += reader.next ?: break
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
_conversations.value = threads
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
private fun latestUnapprovedConversationTimestamp() = reloadTriggersAndContentChanges()
|
||||
.map { threadDb.latestUnapprovedConversationTimestamp }
|
||||
|
||||
@Suppress("OPT_IN_USAGE")
|
||||
private fun observeConversationList(): Flow<List<ThreadRecord>> = reloadTriggersAndContentChanges()
|
||||
.mapLatest { _ ->
|
||||
threadDb.approvedConversationList.use { openCursor ->
|
||||
threadDb.readerFor(openCursor).run { generateSequence { next }.toList() }
|
||||
}
|
||||
}
|
||||
return conversations
|
||||
}
|
||||
|
||||
}
|
||||
@OptIn(FlowPreview::class)
|
||||
private fun reloadTriggersAndContentChanges() = merge(
|
||||
manualReloadTrigger,
|
||||
contentResolver.observeChanges(DatabaseContentProviders.ConversationList.CONTENT_URI)
|
||||
)
|
||||
.flowOn(Dispatchers.IO)
|
||||
.debounce(CHANGE_NOTIFICATION_DEBOUNCE_MILLS)
|
||||
.onStart { emit(Unit) }
|
||||
|
||||
fun tryReload() = manualReloadTrigger.tryEmit(Unit)
|
||||
|
||||
data class Data(
|
||||
val threads: List<ThreadRecord> = emptyList(),
|
||||
val typingThreadIDs: Set<Long> = emptySet(),
|
||||
val messageRequests: MessageRequests? = null
|
||||
)
|
||||
|
||||
fun createMessageRequests(
|
||||
count: Int,
|
||||
hidden: Boolean,
|
||||
timestamp: Long
|
||||
) = if (count > 0 && !hidden) MessageRequests(
|
||||
count.toString(),
|
||||
DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), timestamp)
|
||||
) else null
|
||||
|
||||
data class MessageRequests(val count: String, val timestamp: String)
|
||||
|
||||
companion object {
|
||||
private const val CHANGE_NOTIFICATION_DEBOUNCE_MILLS = 100L
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,31 @@
|
||||
package org.thoughtcrime.securesms.util
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.database.ContentObserver
|
||||
import android.net.Uri
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.annotation.CheckResult
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
|
||||
/**
|
||||
* Observe changes to a content Uri. This function will emit the Uri whenever the content or
|
||||
* its descendants change, according to the parameter [notifyForDescendants].
|
||||
*/
|
||||
@CheckResult
|
||||
fun ContentResolver.observeChanges(uri: Uri, notifyForDescendants: Boolean = false): Flow<Uri> {
|
||||
return callbackFlow {
|
||||
val observer = object : ContentObserver(Handler(Looper.getMainLooper())) {
|
||||
override fun onChange(selfChange: Boolean) {
|
||||
trySend(uri)
|
||||
}
|
||||
}
|
||||
|
||||
registerContentObserver(uri, notifyForDescendants, observer)
|
||||
awaitClose {
|
||||
unregisterContentObserver(observer)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,100 +1,121 @@
|
||||
#include "contacts.h"
|
||||
#include "util.h"
|
||||
#include "jni_utils.h"
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_network_loki_messenger_libsession_1util_Contacts_get(JNIEnv *env, jobject thiz,
|
||||
jstring session_id) {
|
||||
std::lock_guard lock{util::util_mutex_};
|
||||
auto contacts = ptrToContacts(env, thiz);
|
||||
auto session_id_chars = env->GetStringUTFChars(session_id, nullptr);
|
||||
auto contact = contacts->get(session_id_chars);
|
||||
env->ReleaseStringUTFChars(session_id, session_id_chars);
|
||||
if (!contact) return nullptr;
|
||||
jobject j_contact = serialize_contact(env, contact.value());
|
||||
return j_contact;
|
||||
// If an exception is thrown, return nullptr
|
||||
return jni_utils::run_catching_cxx_exception_or<jobject>(
|
||||
[=]() -> jobject {
|
||||
std::lock_guard lock{util::util_mutex_};
|
||||
auto contacts = ptrToContacts(env, thiz);
|
||||
auto session_id_chars = env->GetStringUTFChars(session_id, nullptr);
|
||||
auto contact = contacts->get(session_id_chars);
|
||||
env->ReleaseStringUTFChars(session_id, session_id_chars);
|
||||
if (!contact) return nullptr;
|
||||
jobject j_contact = serialize_contact(env, contact.value());
|
||||
return j_contact;
|
||||
},
|
||||
[](const char *) -> jobject { return nullptr; }
|
||||
);
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_network_loki_messenger_libsession_1util_Contacts_getOrConstruct(JNIEnv *env, jobject thiz,
|
||||
jstring session_id) {
|
||||
std::lock_guard lock{util::util_mutex_};
|
||||
auto contacts = ptrToContacts(env, thiz);
|
||||
auto session_id_chars = env->GetStringUTFChars(session_id, nullptr);
|
||||
auto contact = contacts->get_or_construct(session_id_chars);
|
||||
env->ReleaseStringUTFChars(session_id, session_id_chars);
|
||||
return serialize_contact(env, contact);
|
||||
return jni_utils::run_catching_cxx_exception_or_throws<jobject>(env, [=] {
|
||||
std::lock_guard lock{util::util_mutex_};
|
||||
auto contacts = ptrToContacts(env, thiz);
|
||||
auto session_id_chars = env->GetStringUTFChars(session_id, nullptr);
|
||||
auto contact = contacts->get_or_construct(session_id_chars);
|
||||
env->ReleaseStringUTFChars(session_id, session_id_chars);
|
||||
return serialize_contact(env, contact);
|
||||
});
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_network_loki_messenger_libsession_1util_Contacts_set(JNIEnv *env, jobject thiz,
|
||||
jobject contact) {
|
||||
std::lock_guard lock{util::util_mutex_};
|
||||
auto contacts = ptrToContacts(env, thiz);
|
||||
auto contact_info = deserialize_contact(env, contact, contacts);
|
||||
contacts->set(contact_info);
|
||||
jni_utils::run_catching_cxx_exception_or_throws<void>(env, [=] {
|
||||
std::lock_guard lock{util::util_mutex_};
|
||||
auto contacts = ptrToContacts(env, thiz);
|
||||
auto contact_info = deserialize_contact(env, contact, contacts);
|
||||
contacts->set(contact_info);
|
||||
});
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_network_loki_messenger_libsession_1util_Contacts_erase(JNIEnv *env, jobject thiz,
|
||||
jstring session_id) {
|
||||
std::lock_guard lock{util::util_mutex_};
|
||||
auto contacts = ptrToContacts(env, thiz);
|
||||
auto session_id_chars = env->GetStringUTFChars(session_id, nullptr);
|
||||
return jni_utils::run_catching_cxx_exception_or_throws<jboolean>(env, [=] {
|
||||
std::lock_guard lock{util::util_mutex_};
|
||||
auto contacts = ptrToContacts(env, thiz);
|
||||
auto session_id_chars = env->GetStringUTFChars(session_id, nullptr);
|
||||
|
||||
bool result = contacts->erase(session_id_chars);
|
||||
env->ReleaseStringUTFChars(session_id, session_id_chars);
|
||||
return result;
|
||||
bool result = contacts->erase(session_id_chars);
|
||||
env->ReleaseStringUTFChars(session_id, session_id_chars);
|
||||
return result;
|
||||
});
|
||||
}
|
||||
extern "C"
|
||||
#pragma clang diagnostic push
|
||||
#pragma ide diagnostic ignored "bugprone-reserved-identifier"
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_network_loki_messenger_libsession_1util_Contacts_00024Companion_newInstance___3B(JNIEnv *env,
|
||||
jobject thiz,
|
||||
jbyteArray ed25519_secret_key) {
|
||||
std::lock_guard lock{util::util_mutex_};
|
||||
auto secret_key = util::ustring_from_bytes(env, ed25519_secret_key);
|
||||
auto* contacts = new session::config::Contacts(secret_key, std::nullopt);
|
||||
jobject thiz,
|
||||
jbyteArray ed25519_secret_key) {
|
||||
return jni_utils::run_catching_cxx_exception_or_throws<jobject>(env, [=] {
|
||||
std::lock_guard lock{util::util_mutex_};
|
||||
auto secret_key = util::ustring_from_bytes(env, ed25519_secret_key);
|
||||
auto *contacts = new session::config::Contacts(secret_key, std::nullopt);
|
||||
|
||||
jclass contactsClass = env->FindClass("network/loki/messenger/libsession_util/Contacts");
|
||||
jmethodID constructor = env->GetMethodID(contactsClass, "<init>", "(J)V");
|
||||
jobject newConfig = env->NewObject(contactsClass, constructor, reinterpret_cast<jlong>(contacts));
|
||||
jclass contactsClass = env->FindClass("network/loki/messenger/libsession_util/Contacts");
|
||||
jmethodID constructor = env->GetMethodID(contactsClass, "<init>", "(J)V");
|
||||
jobject newConfig = env->NewObject(contactsClass, constructor,
|
||||
reinterpret_cast<jlong>(contacts));
|
||||
|
||||
return newConfig;
|
||||
return newConfig;
|
||||
});
|
||||
}
|
||||
extern "C"
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_network_loki_messenger_libsession_1util_Contacts_00024Companion_newInstance___3B_3B(
|
||||
JNIEnv *env, jobject thiz, jbyteArray ed25519_secret_key, jbyteArray initial_dump) {
|
||||
std::lock_guard lock{util::util_mutex_};
|
||||
auto secret_key = util::ustring_from_bytes(env, ed25519_secret_key);
|
||||
auto initial = util::ustring_from_bytes(env, initial_dump);
|
||||
return jni_utils::run_catching_cxx_exception_or_throws<jobject>(env, [=] {
|
||||
std::lock_guard lock{util::util_mutex_};
|
||||
auto secret_key = util::ustring_from_bytes(env, ed25519_secret_key);
|
||||
auto initial = util::ustring_from_bytes(env, initial_dump);
|
||||
|
||||
auto* contacts = new session::config::Contacts(secret_key, initial);
|
||||
auto *contacts = new session::config::Contacts(secret_key, initial);
|
||||
|
||||
jclass contactsClass = env->FindClass("network/loki/messenger/libsession_util/Contacts");
|
||||
jmethodID constructor = env->GetMethodID(contactsClass, "<init>", "(J)V");
|
||||
jobject newConfig = env->NewObject(contactsClass, constructor, reinterpret_cast<jlong>(contacts));
|
||||
jclass contactsClass = env->FindClass("network/loki/messenger/libsession_util/Contacts");
|
||||
jmethodID constructor = env->GetMethodID(contactsClass, "<init>", "(J)V");
|
||||
jobject newConfig = env->NewObject(contactsClass, constructor,
|
||||
reinterpret_cast<jlong>(contacts));
|
||||
|
||||
return newConfig;
|
||||
return newConfig;
|
||||
});
|
||||
}
|
||||
#pragma clang diagnostic pop
|
||||
extern "C"
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_network_loki_messenger_libsession_1util_Contacts_all(JNIEnv *env, jobject thiz) {
|
||||
std::lock_guard lock{util::util_mutex_};
|
||||
auto contacts = ptrToContacts(env, thiz);
|
||||
jclass stack = env->FindClass("java/util/Stack");
|
||||
jmethodID init = env->GetMethodID(stack, "<init>", "()V");
|
||||
jobject our_stack = env->NewObject(stack, init);
|
||||
jmethodID push = env->GetMethodID(stack, "push", "(Ljava/lang/Object;)Ljava/lang/Object;");
|
||||
for (const auto& contact : *contacts) {
|
||||
auto contact_obj = serialize_contact(env, contact);
|
||||
env->CallObjectMethod(our_stack, push, contact_obj);
|
||||
}
|
||||
return our_stack;
|
||||
return jni_utils::run_catching_cxx_exception_or_throws<jobject>(env, [=] {
|
||||
std::lock_guard lock{util::util_mutex_};
|
||||
auto contacts = ptrToContacts(env, thiz);
|
||||
jclass stack = env->FindClass("java/util/Stack");
|
||||
jmethodID init = env->GetMethodID(stack, "<init>", "()V");
|
||||
jobject our_stack = env->NewObject(stack, init);
|
||||
jmethodID push = env->GetMethodID(stack, "push", "(Ljava/lang/Object;)Ljava/lang/Object;");
|
||||
for (const auto &contact: *contacts) {
|
||||
auto contact_obj = serialize_contact(env, contact);
|
||||
env->CallObjectMethod(our_stack, push, contact_obj);
|
||||
}
|
||||
return our_stack;
|
||||
});
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
#ifndef SESSION_ANDROID_JNI_UTILS_H
|
||||
#define SESSION_ANDROID_JNI_UTILS_H
|
||||
|
||||
#include <jni.h>
|
||||
#include <exception>
|
||||
|
||||
namespace jni_utils {
|
||||
/**
|
||||
* Run a C++ function and catch any exceptions, throwing a Java exception if one is caught,
|
||||
* and returning a default-constructed value of the specified type.
|
||||
*
|
||||
* @tparam RetT The return type of the function
|
||||
* @tparam Func The function type
|
||||
* @param f The function to run
|
||||
* @param fallbackRun The function to run if an exception is caught. The optional exception message reference will be passed to this function.
|
||||
* @return The return value of the function, or the return value of the fallback function if an exception was caught
|
||||
*/
|
||||
template<class RetT, class Func, class FallbackRun>
|
||||
RetT run_catching_cxx_exception_or(Func f, FallbackRun fallbackRun) {
|
||||
try {
|
||||
return f();
|
||||
} catch (const std::exception &e) {
|
||||
return fallbackRun(e.what());
|
||||
} catch (...) {
|
||||
return fallbackRun(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a C++ function and catch any exceptions, throwing a Java exception if one is caught.
|
||||
*
|
||||
* @tparam RetT The return type of the function
|
||||
* @tparam Func The function type
|
||||
* @param env The JNI environment
|
||||
* @param f The function to run
|
||||
* @return The return value of the function, or a default-constructed value of the specified type if an exception was caught
|
||||
*/
|
||||
template<class RetT, class Func>
|
||||
RetT run_catching_cxx_exception_or_throws(JNIEnv *env, Func f) {
|
||||
return run_catching_cxx_exception_or<RetT>(f, [env](const char *msg) {
|
||||
jclass exceptionClass = env->FindClass("java/lang/RuntimeException");
|
||||
if (msg) {
|
||||
auto formatted_message = std::string("libsession: C++ exception: ") + msg;
|
||||
env->ThrowNew(exceptionClass, formatted_message.c_str());
|
||||
} else {
|
||||
env->ThrowNew(exceptionClass, "libsession: Unknown C++ exception");
|
||||
}
|
||||
|
||||
return RetT();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#endif //SESSION_ANDROID_JNI_UTILS_H
|
Loading…
Reference in New Issue