From 39dd36433058dba36fdcbd561d9af9e5ab50570f Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Tue, 24 Feb 2026 23:47:49 -0500 Subject: [PATCH 1/4] WIP --- .../react-native-kb/android/CMakeLists.txt | 4 +- .../react-native-kb/android/cpp-adapter.cpp | 162 +++++------- .../main/java/com/reactnativekb/KbModule.kt | 50 ++-- .../react-native-kb/cpp/react-native-kb.cpp | 245 +++++++++++------- .../react-native-kb/cpp/react-native-kb.h | 48 +++- .../react-native-kb/ios/KBJSScheduler.cpp | 26 -- rnmodules/react-native-kb/ios/KBJSScheduler.h | 29 --- rnmodules/react-native-kb/ios/Kb.h | 3 +- rnmodules/react-native-kb/ios/Kb.mm | 181 ++++--------- rnmodules/react-native-kb/src/index.tsx | 26 +- 10 files changed, 331 insertions(+), 443 deletions(-) delete mode 100644 rnmodules/react-native-kb/ios/KBJSScheduler.cpp delete mode 100644 rnmodules/react-native-kb/ios/KBJSScheduler.h diff --git a/rnmodules/react-native-kb/android/CMakeLists.txt b/rnmodules/react-native-kb/android/CMakeLists.txt index c016c8798d8a..37372ea7a10a 100644 --- a/rnmodules/react-native-kb/android/CMakeLists.txt +++ b/rnmodules/react-native-kb/android/CMakeLists.txt @@ -2,7 +2,7 @@ project(cpp) cmake_minimum_required(VERSION 3.4.1) set (CMAKE_VERBOSE_MAKEFILE ON) -set (CMAKE_CXX_STANDARD 17) +set (CMAKE_CXX_STANDARD 20) set (NODE_MODULES_DIR "${CMAKE_SOURCE_DIR}/../..") add_library(cpp @@ -21,7 +21,7 @@ include_directories( set_target_properties( cpp PROPERTIES - CXX_STANDARD 17 + CXX_STANDARD 20 CXX_EXTENSIONS OFF POSITION_INDEPENDENT_CODE ON ) diff --git a/rnmodules/react-native-kb/android/cpp-adapter.cpp b/rnmodules/react-native-kb/android/cpp-adapter.cpp index f7ed9c8da652..5b77f7204890 100644 --- a/rnmodules/react-native-kb/android/cpp-adapter.cpp +++ b/rnmodules/react-native-kb/android/cpp-adapter.cpp @@ -1,36 +1,31 @@ -// https://github.com/ammarahm-ed/react-native-jsi-template/blob/master/android/cpp-adapter.cpp -#include "pthread.h" #include "react-native-kb.h" +#include #include -#include +#include #include #include #include +#include #include using namespace facebook; using namespace facebook::jsi; -using namespace std; -using namespace kb; +using namespace facebook::react; -JavaVM *java_vm = NULL; -jclass java_class; -jobject java_object; +static std::shared_ptr g_bridge; +static JavaVM *java_vm = nullptr; +static jobject java_object = nullptr; +static jclass g_kbClass = nullptr; +static jmethodID g_rpcOnGoMethod = nullptr; /** * A simple callback function that allows us to detach current JNI Environment - * when the thread + * when the thread is destroyed. * See https://stackoverflow.com/a/30026231 for detailed explanation */ - void DeferThreadDetach(JNIEnv *env) { static pthread_key_t thread_key; - // Set up a Thread Specific Data key, and a callback that - // will be executed when a thread is destroyed. - // This is only done once, across all threads, and the value - // associated with the key for any given thread will initially - // be NULL. static auto run_once = [] { const auto err = pthread_key_create(&thread_key, [](void *ts_env) { if (ts_env) { @@ -38,20 +33,14 @@ void DeferThreadDetach(JNIEnv *env) { } }); if (err) { - // Failed to create TSD key. Throw an exception if you want to. } return 0; }(); static_cast(run_once); - // For the callback to actually be executed when a thread exits - // we need to associate a non-NULL value with the key on that thread. - // We can use the JNIEnv* as that value. const auto ts_env = pthread_getspecific(thread_key); if (!ts_env) { if (pthread_setspecific(thread_key, env)) { - // Failed to set thread-specific value for key. Throw an exception if you - // want to. } } } @@ -59,100 +48,87 @@ void DeferThreadDetach(JNIEnv *env) { /** * Get a JNIEnv* valid for this thread, regardless of whether * we're on a native thread or a Java thread. - * If the calling thread is not currently attached to the JVM - * it will be attached, and then automatically detached when the - * thread is destroyed. - * * See https://stackoverflow.com/a/30026231 for detailed explanation */ JNIEnv *GetJniEnv() { JNIEnv *env = nullptr; - // We still call GetEnv first to detect if the thread already - // is attached. This is done to avoid setting up a DetachCurrentThread - // call on a Java thread. - - // g_vm is a global. auto get_env_result = java_vm->GetEnv((void **)&env, JNI_VERSION_1_6); if (get_env_result == JNI_EDETACHED) { - if (java_vm->AttachCurrentThread(&env, NULL) == JNI_OK) { + if (java_vm->AttachCurrentThread(&env, nullptr) == JNI_OK) { DeferThreadDetach(env); - } else { - // Failed to attach thread. Throw an exception if you want to. } - } else if (get_env_result == JNI_EVERSION) { - // Unsupported JNI version. Throw an exception if you want to. } return env; } -static jstring string2jstring(JNIEnv *env, const string &str) { - return (*env).NewStringUTF(str.c_str()); +static void cacheJNIMethods(JNIEnv *env) { + if (!g_kbClass && java_object) { + jclass cls = env->GetObjectClass(java_object); + g_kbClass = (jclass)env->NewGlobalRef(cls); + g_rpcOnGoMethod = env->GetMethodID(g_kbClass, "rpcOnGo", "([B)V"); + env->DeleteLocalRef(cls); + } } -void install(facebook::jsi::Runtime &jsiRuntime) { - auto rpcOnGo = Function::createFromHostFunction( - jsiRuntime, PropNameID::forAscii(jsiRuntime, "rpcOnGo"), 1, - [](Runtime &runtime, const Value &thisValue, const Value *arguments, - size_t count) -> Value { - return RpcOnGo( - runtime, thisValue, arguments, count, [](void *ptr, size_t size) { +static jni::local_ref +getBindingsInstaller(jni::alias_ref thiz) { + JNIEnv *env = jni::Environment::current(); + env->GetJavaVM(&java_vm); + if (java_object) { + env->DeleteGlobalRef(java_object); + } + java_object = env->NewGlobalRef(thiz.get()); + cacheJNIMethods(env); + + return BindingsInstallerHolder::newObjectCxxArgs( + [](jsi::Runtime &runtime, + const std::shared_ptr &callInvoker) { + if (g_bridge) { + g_bridge->teardown(); + } + g_bridge = std::make_shared(); + g_bridge->install( + runtime, callInvoker, + // writeToGo: called from JS thread when rpcOnGo fires + [](void *ptr, size_t size) { JNIEnv *jniEnv = GetJniEnv(); - java_class = jniEnv->GetObjectClass(java_object); - jmethodID rpcOnGo = - jniEnv->GetMethodID(java_class, "rpcOnGo", "([B)V"); + if (!jniEnv || !java_object || !g_rpcOnGoMethod) + return; jbyteArray jba = jniEnv->NewByteArray(size); jniEnv->SetByteArrayRegion(jba, 0, size, (jbyte *)ptr); - jvalue params[1]; - params[0].l = jba; - jniEnv->CallVoidMethodA(java_object, rpcOnGo, params); + jniEnv->CallVoidMethod(java_object, g_rpcOnGoMethod, jba); + jniEnv->DeleteLocalRef(jba); + }, + // onError + [](const std::string &err) { + __android_log_print(ANDROID_LOG_ERROR, "KBBridge", + "JSI error: %s", err.c_str()); }); }); - jsiRuntime.global().setProperty(jsiRuntime, "rpcOnGo", std::move(rpcOnGo)); } -extern "C" JNIEXPORT void JNICALL installJSI(JNIEnv *env, jobject thiz, jlong jsi) { - auto runtime = reinterpret_cast(jsi); - if (runtime) { - install(*runtime); - } - env->GetJavaVM(&java_vm); - java_object = env->NewGlobalRef(thiz); +static void nativeOnDataFromGo(jni::alias_ref thiz, + jni::alias_ref data) { + auto bridge = g_bridge; + if (!bridge || !data) + return; + JNIEnv *env = jni::Environment::current(); + auto rawArray = data.get(); + auto size = static_cast(env->GetArrayLength(rawArray)); + auto bytes = + reinterpret_cast(env->GetByteArrayElements(rawArray, nullptr)); + bridge->onDataFromGo(bytes, size); + env->ReleaseByteArrayElements(rawArray, reinterpret_cast(bytes), + JNI_ABORT); } -extern "C" JNIEXPORT void JNICALL emit(JNIEnv *env, jclass clazz, jlong jsi, jobject boxedCallInvokerHolder, jbyteArray data) { - auto rPtr = reinterpret_cast(jsi); - auto &runtime = *rPtr; - auto boxedCallInvokerRef = jni::make_local(boxedCallInvokerHolder); - auto callInvokerHolder = - jni::dynamic_ref_cast( - boxedCallInvokerRef); - auto callInvoker = callInvokerHolder->cthis()->getCallInvoker(); - - auto size = static_cast(env->GetArrayLength(data)); - auto payloadBytes = - reinterpret_cast(env->GetByteArrayElements(data, nullptr)); - auto values = PrepRpcOnJS(runtime, payloadBytes, size); - callInvoker->invokeAsync([values, &runtime]() { - RpcOnJS(runtime, values, [](const std::string &err) { - JNIEnv *jniEnv = GetJniEnv(); - java_class = jniEnv->GetObjectClass(java_object); - jmethodID log = - jniEnv->GetMethodID(java_class, "log", "(Ljava/lang/String;)V"); - auto s = string2jstring(jniEnv, err); - jvalue params[1]; - params[0].l = s; - jniEnv->CallVoidMethodA(java_object, log, params); - }); +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) { + java_vm = vm; + return jni::initialize(vm, [] { + jni::findClassStatic("com/reactnativekb/KbModule") + ->registerNatives({ + makeNativeMethod("getBindingsInstaller", getBindingsInstaller), + makeNativeMethod("nativeOnDataFromGo", nativeOnDataFromGo), + }); }); } - -static JNINativeMethod methods[] = { - {"installJSI", "(J)V", (void *)&installJSI}, - {"emit", "(JLcom/facebook/react/turbomodule/core/CallInvokerHolderImpl;[B)V", (void *)&emit}, -}; - - -extern "C" JNIEXPORT void JNICALL Java_com_reactnativekb_KbModule_registerNatives(JNIEnv *env, jobject thiz, jlong jsi) { - jclass clazz = env->FindClass("com/reactnativekb/KbModule"); - env->RegisterNatives(clazz, methods, sizeof(methods)/sizeof(methods[0])); -} diff --git a/rnmodules/react-native-kb/android/src/main/java/com/reactnativekb/KbModule.kt b/rnmodules/react-native-kb/android/src/main/java/com/reactnativekb/KbModule.kt index db95666068a6..1d1d1dc053d0 100644 --- a/rnmodules/react-native-kb/android/src/main/java/com/reactnativekb/KbModule.kt +++ b/rnmodules/react-native-kb/android/src/main/java/com/reactnativekb/KbModule.kt @@ -33,7 +33,9 @@ import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.WritableArray import com.facebook.react.bridge.WritableMap import com.facebook.react.module.annotations.ReactModule -import com.facebook.react.turbomodule.core.CallInvokerHolderImpl +import com.facebook.react.turbomodule.core.interfaces.TurboModuleWithJSIBindings +import com.facebook.react.turbomodule.core.interfaces.BindingsInstallerHolder +import com.facebook.proguard.annotations.DoNotStrip import com.google.android.gms.tasks.OnCompleteListener import com.google.android.gms.tasks.Task import com.google.firebase.messaging.FirebaseMessagingService @@ -61,7 +63,6 @@ import me.leolin.shortcutbadger.ShortcutBadger import keybase.Keybase.readArr import keybase.Keybase.version import keybase.Keybase.writeArr -import com.facebook.react.common.annotations.FrameworkAPI import android.media.MediaMetadataRetriever import androidx.media3.transformer.TransformationRequest import androidx.media3.transformer.Transformer @@ -79,16 +80,16 @@ import androidx.media3.transformer.DefaultEncoderFactory import java.nio.ByteBuffer import kotlin.math.min -@OptIn(FrameworkAPI::class) -class KbModule(reactContext: ReactApplicationContext?) : KbSpec(reactContext) { +class KbModule(reactContext: ReactApplicationContext?) : KbSpec(reactContext), TurboModuleWithJSIBindings { private val misTestDevice: Boolean private val initialIntent: HashMap? = null private val reactContext: ReactApplicationContext - private external fun registerNatives(jsiPtr: Long) - private external fun installJSI(jsiPtr: Long) - private external fun emit(jsiPtr: Long, jsInvoker: CallInvokerHolderImpl?, data: ByteArray?) + + @DoNotStrip + external override fun getBindingsInstaller(): BindingsInstallerHolder + private external fun nativeOnDataFromGo(data: ByteArray) + private var executor: ExecutorService? = null - private var jsiInstalled: Boolean? = false override fun getName(): String { return NAME @@ -797,20 +798,8 @@ class KbModule(reactContext: ReactApplicationContext?) : KbSpec(reactContext) { @ReactMethod(isBlockingSynchronousMethod = true) override fun install(): Boolean { - try { - System.loadLibrary("cpp") - jsiInstalled = true - val jsi = reactContext.javaScriptContextHolder?.get() - if (jsi != null) { - registerNatives(jsi) - installJSI(jsi) - } else { - throw Exception("No context holder") - } - } catch (exception: Exception) { - NativeLogger.error("Exception in installJSI", exception) - } - return true; + // No-op: JSI bindings are now installed via TurboModuleWithJSIBindings.getBindingsInstaller() + return true } @ReactMethod @@ -875,17 +864,10 @@ class KbModule(reactContext: ReactApplicationContext?) : KbSpec(reactContext) { Thread.currentThread().setName("ReadFromKBLib") val data: ByteArray = readArr() if (!reactContext.hasActiveReactInstance()) { - NativeLogger.info(NAME.toString() + ": JS Bridge is dead, dropping engine message: " + data) - - } - - val callInvoker: CallInvokerHolderImpl = reactContext.getJSCallInvokerHolder() as CallInvokerHolderImpl - val jsi = reactContext.javaScriptContextHolder?.get() - if (jsi != null) { - emit(jsi, callInvoker, data) - } else { - throw Exception("No context holder") + NativeLogger.info("$NAME: JS Bridge is dead, dropping engine message") + continue } + nativeOnDataFromGo(data) } catch (e: Exception) { if (e.message != null && e.message.equals("Read error: EOF")) { NativeLogger.info("Got EOF from read. Likely because of reset.") @@ -939,6 +921,10 @@ class KbModule(reactContext: ReactApplicationContext?) : KbSpec(reactContext) { } companion object { + init { + System.loadLibrary("cpp") + } + const val NAME: String = "Kb" private val RN_NAME: String = "ReactNativeJS" private val RPC_META_EVENT_NAME: String = "kb-meta-engine-event" diff --git a/rnmodules/react-native-kb/cpp/react-native-kb.cpp b/rnmodules/react-native-kb/cpp/react-native-kb.cpp index babec40e935f..0624469f8688 100644 --- a/rnmodules/react-native-kb/cpp/react-native-kb.cpp +++ b/rnmodules/react-native-kb/cpp/react-native-kb.cpp @@ -1,35 +1,33 @@ #include "react-native-kb.h" -#include -#include +#include #include using namespace facebook; using namespace facebook::jsi; namespace kb { -std::atomic isTornDown{false}; -void Teardown() { isTornDown.store(true); } +void KBBridge::teardown() { + isTornDown_.store(true); + // Clear cached JSI objects while the runtime is still alive. + // This prevents stale jsi::Function destructors from crashing + // if the bridge outlives the runtime (due to shared_ptr captures). + cachedUint8ArrayCtor_.reset(); + cachedRpcOnJs_.reset(); + cachedRuntime_ = nullptr; +} -void Tearup() { isTornDown.store(false); } +void KBBridge::tearup() { isTornDown_.store(false); } -Value RpcOnGo(Runtime &runtime, const Value &thisValue, const Value *arguments, - size_t count, void (*callback)(void *ptr, size_t size)) { - try { - auto obj = arguments[0].asObject(runtime); - auto buffer = obj.getArrayBuffer(runtime); - auto ptr = buffer.data(runtime); - auto size = buffer.size(runtime); - callback(ptr, size); - return Value(true); - } catch (const std::exception &e) { - throw std::runtime_error("Error in RpcOnGo: " + std::string(e.what())); - } catch (...) { - throw std::runtime_error("Unknown error in RpcOnGo"); +void KBBridge::resetCaches(Runtime &runtime) { + if (cachedRuntime_ != &runtime) { + cachedUint8ArrayCtor_.reset(); + cachedRpcOnJs_.reset(); + cachedRuntime_ = &runtime; } } -std::string mpToString(msgpack::object &o) { +static std::string mpToString(msgpack::object &o) { switch (o.type) { case msgpack::type::STR: return o.as(); @@ -46,7 +44,7 @@ std::string mpToString(msgpack::object &o) { } } -Value convertMPToJSI(Runtime &runtime, msgpack::object &o) { +Value KBBridge::convertMPToJSI(Runtime &runtime, msgpack::object &o) { switch (o.type) { case msgpack::type::STR: return jsi::String::createFromUtf8(runtime, o.as()); @@ -79,30 +77,28 @@ Value convertMPToJSI(Runtime &runtime, msgpack::object &o) { auto ptr = o.via.bin.ptr; int size = o.via.bin.size; - // make ArrayBuffer and copy in data - // non-owning - static Function* cachedUint8ArrayCtor = nullptr; - static Runtime* cachedRuntime = nullptr; - if (cachedRuntime != &runtime) { - cachedUint8ArrayCtor = nullptr; - cachedRuntime = &runtime; - } - if (!cachedUint8ArrayCtor) { - auto ctor = runtime.global().getPropertyAsFunction(runtime, "Uint8Array"); - cachedUint8ArrayCtor = new Function(std::move(ctor)); + resetCaches(runtime); + if (!cachedUint8ArrayCtor_) { + auto ctor = + runtime.global().getPropertyAsFunction(runtime, "Uint8Array"); + cachedUint8ArrayCtor_ = std::make_unique(std::move(ctor)); } - Value uint8Array = cachedUint8ArrayCtor->callAsConstructor(runtime, size); + Value uint8Array = + cachedUint8ArrayCtor_->callAsConstructor(runtime, size); Object uint8ArrayObj = uint8Array.asObject(runtime); - ArrayBuffer buffer = uint8ArrayObj.getProperty(runtime, "buffer").asObject(runtime).getArrayBuffer(runtime); + ArrayBuffer buffer = uint8ArrayObj.getProperty(runtime, "buffer") + .asObject(runtime) + .getArrayBuffer(runtime); std::memcpy(buffer.data(runtime), ptr, size); return uint8Array; } case msgpack::type::ARRAY: { auto size = o.via.array.size; jsi::Array arr(runtime, size); - for (int i = 0; i < size; ++i) { - arr.setValueAtIndex(runtime, i, convertMPToJSI(runtime, o.via.array.ptr[i])); + for (uint32_t i = 0; i < size; ++i) { + arr.setValueAtIndex(runtime, i, + convertMPToJSI(runtime, o.via.array.ptr[i])); } return arr; } @@ -111,81 +107,144 @@ Value convertMPToJSI(Runtime &runtime, msgpack::object &o) { } } -enum class ReadState { needSize, needContent }; -ReadState g_state = ReadState::needSize; -msgpack::unpacker unp; +void KBBridge::install( + Runtime &runtime, + std::shared_ptr callInvoker, + std::function writeToGo, + std::function onError) { + callInvoker_ = std::move(callInvoker); + onError_ = std::move(onError); + + auto rpcOnGo = Function::createFromHostFunction( + runtime, PropNameID::forAscii(runtime, "rpcOnGo"), 1, + [writeToGo = std::move(writeToGo)](Runtime &runtime, + const Value &thisValue, + const Value *arguments, + size_t count) -> Value { + try { + auto obj = arguments[0].asObject(runtime); + auto buffer = obj.getArrayBuffer(runtime); + auto ptr = buffer.data(runtime); + auto size = buffer.size(runtime); + writeToGo(ptr, size); + return Value(true); + } catch (const std::exception &e) { + throw std::runtime_error("Error in rpcOnGo: " + + std::string(e.what())); + } catch (...) { + throw std::runtime_error("Unknown error in rpcOnGo"); + } + }); + + runtime.global().setProperty(runtime, "rpcOnGo", std::move(rpcOnGo)); + + // HostObject that calls teardown when the JS runtime is destroyed + class KBTearDownSimple : public jsi::HostObject { + public: + KBTearDownSimple(std::weak_ptr bridge) : bridge_(bridge) { + if (auto b = bridge_.lock()) { + b->tearup(); + } + } + ~KBTearDownSimple() override { + if (auto b = bridge_.lock()) { + b->teardown(); + } + } + Value get(Runtime &, const PropNameID &) override { + return Value::undefined(); + } + void set(Runtime &, const PropNameID &, const Value &) override {} + std::vector getPropertyNames(Runtime &) override { return {}; } + + private: + std::weak_ptr bridge_; + }; + + runtime.global().setProperty( + runtime, "kbTeardown", + Object::createFromHostObject( + runtime, + std::make_shared(shared_from_this()))); +} + +void KBBridge::onDataFromGo(uint8_t *data, int size) { + if (isTornDown_.load() || size <= 0) { + return; + } -ShareValues PrepRpcOnJS(Runtime &runtime, uint8_t *data, int size) { try { auto values = std::make_shared>(); - if (size > 0) { - unp.reserve_buffer(size); - std::copy(data, data + size, unp.buffer()); - unp.buffer_consumed(size); - while (true) { - msgpack::object_handle result; - if (unp.next(result)) { - if (g_state == ReadState::needSize) { - g_state = ReadState::needContent; - } else { - values->push_back(std::move(result)); - g_state = ReadState::needSize; - } + unpacker_.reserve_buffer(size); + std::copy(data, data + size, unpacker_.buffer()); + unpacker_.buffer_consumed(size); + while (true) { + msgpack::object_handle result; + if (unpacker_.next(result)) { + if (readState_ == ReadState::needSize) { + readState_ = ReadState::needContent; } else { - break; + values->push_back(std::move(result)); + readState_ = ReadState::needSize; } + } else { + break; } } - return values; - } catch (const std::exception &e) { - throw std::runtime_error("Error in PrepRpcOnJS: " + - std::string(e.what())); - } catch (...) { - throw std::runtime_error("Unknown error in PrepRpcOnJS"); - } -} -void RpcOnJS(Runtime &runtime, ShareValues values, void (*err_callback)(const std::string &err)) { - try { - if (isTornDown.load()) { + if (values->empty()) { return; } - // non-owning - static Function* cachedRpcOnJs = nullptr; - static Runtime* cachedRuntime = nullptr; + auto self = shared_from_this(); + callInvoker_->invokeAsync([values, self](jsi::Runtime &runtime) { + try { + if (self->isTornDown_.load()) { + return; + } - if (cachedRuntime != &runtime) { - cachedRpcOnJs = nullptr; - cachedRuntime = &runtime; - } + self->resetCaches(runtime); + if (!self->cachedRpcOnJs_) { + try { + auto func = + runtime.global().getPropertyAsFunction(runtime, "rpcOnJs"); + self->cachedRpcOnJs_ = + std::make_unique(std::move(func)); + } catch (...) { + if (self->onError_) { + self->onError_("Failed to get rpcOnJs function"); + } + return; + } + } - if (!cachedRpcOnJs) { - try { - auto func = runtime.global().getPropertyAsFunction(runtime, "rpcOnJs"); - cachedRpcOnJs = new Function(std::move(func)); + for (auto &result : *values) { + msgpack::object obj(result.get()); + Value value = self->convertMPToJSI(runtime, obj); + if (self->isTornDown_.load()) { + return; + } + self->cachedRpcOnJs_->call(runtime, std::move(value), 1); + } + } catch (const std::exception &e) { + if (self->onError_) { + self->onError_(e.what()); + } } catch (...) { - err_callback("Failed to get rpcOnJs function"); - throw std::runtime_error("Failed to get rpcOnJs function:"); - return; - } - } - - for (auto &result : *values) { - msgpack::object obj(result.get()); - Value value = convertMPToJSI(runtime, obj); - if (isTornDown.load()) { - return; + if (self->onError_) { + self->onError_("unknown error in onDataFromGo JS callback"); + } } - Function rpcOnJs = runtime.global().getPropertyAsFunction(runtime, "rpcOnJs"); - rpcOnJs.call(runtime, std::move(value), 1); - } + }); } catch (const std::exception &e) { - err_callback(e.what()); - throw std::runtime_error("Error in RpcOnJS: " + std::string(e.what())); + if (onError_) { + onError_(std::string("Error in onDataFromGo: ") + e.what()); + } } catch (...) { - err_callback("unknown error"); - throw std::runtime_error("Unknown error in RpcOnJS"); + if (onError_) { + onError_("Unknown error in onDataFromGo"); + } } } + } // namespace kb diff --git a/rnmodules/react-native-kb/cpp/react-native-kb.h b/rnmodules/react-native-kb/cpp/react-native-kb.h index e3a2dc67d41f..c92a3ec9eec0 100644 --- a/rnmodules/react-native-kb/cpp/react-native-kb.h +++ b/rnmodules/react-native-kb/cpp/react-native-kb.h @@ -1,22 +1,44 @@ #pragma once +#include +#include #include #include +#include #include #include #include +#include +#include namespace kb { -facebook::jsi::Value RpcOnGo(facebook::jsi::Runtime &runtime, - const facebook::jsi::Value &thisValue, - const facebook::jsi::Value *arguments, - size_t count, - void (*callback)(void *ptr, size_t size)); - -typedef std::shared_ptr> ShareValues; -ShareValues PrepRpcOnJS(facebook::jsi::Runtime &runtime, uint8_t *data, - int size); -void RpcOnJS(facebook::jsi::Runtime &runtime, ShareValues values, - void (*err_callback)(const std::string &err)); -void Teardown(); -void Tearup(); + +class KBBridge : public std::enable_shared_from_this { +public: + void install(facebook::jsi::Runtime &runtime, + std::shared_ptr callInvoker, + std::function writeToGo, + std::function onError); + + void onDataFromGo(uint8_t *data, int size); + void teardown(); + void tearup(); + +private: + std::shared_ptr callInvoker_; + std::function onError_; + std::atomic isTornDown_{false}; + + enum class ReadState { needSize, needContent }; + ReadState readState_ = ReadState::needSize; + msgpack::unpacker unpacker_; + + std::unique_ptr cachedUint8ArrayCtor_; + std::unique_ptr cachedRpcOnJs_; + facebook::jsi::Runtime *cachedRuntime_ = nullptr; + + void resetCaches(facebook::jsi::Runtime &runtime); + facebook::jsi::Value convertMPToJSI(facebook::jsi::Runtime &runtime, + msgpack::object &o); +}; + } // namespace kb diff --git a/rnmodules/react-native-kb/ios/KBJSScheduler.cpp b/rnmodules/react-native-kb/ios/KBJSScheduler.cpp deleted file mode 100644 index 6898798e006f..000000000000 --- a/rnmodules/react-native-kb/ios/KBJSScheduler.cpp +++ /dev/null @@ -1,26 +0,0 @@ -// https://github.com/software-mansion/react-native-reanimated/blob/main/Common/cpp/Tools/ -#include "./KBJSScheduler.h" -using namespace facebook; -using namespace react; - -KBJSScheduler::KBJSScheduler( jsi::Runtime &rnRuntime, const std::shared_ptr &jsCallInvoker) - : scheduleOnJS([&](KBJob job) { - jsCallInvoker_->invokeAsync( - [job = std::move(job), &rt = rnRuntime_] { job(rt); }); - }), - rnRuntime_(rnRuntime), - jsCallInvoker_(jsCallInvoker) {} - -// With `runtimeExecutor`. -KBJSScheduler::KBJSScheduler( jsi::Runtime &rnRuntime, RuntimeExecutor runtimeExecutor) - : scheduleOnJS([&](KBJob job) { - runtimeExecutor_( - [job = std::move(job)](jsi::Runtime &runtime) { job(runtime); }); - }), - rnRuntime_(rnRuntime), - runtimeExecutor_(runtimeExecutor) {} - -const std::shared_ptr KBJSScheduler::getJSCallInvoker() const { - assert( jsCallInvoker_ != nullptr && " Expected jsCallInvoker, got nullptr instead."); - return jsCallInvoker_; -} diff --git a/rnmodules/react-native-kb/ios/KBJSScheduler.h b/rnmodules/react-native-kb/ios/KBJSScheduler.h deleted file mode 100644 index 2166c6056697..000000000000 --- a/rnmodules/react-native-kb/ios/KBJSScheduler.h +++ /dev/null @@ -1,29 +0,0 @@ -// https://github.com/software-mansion/react-native-reanimated/blob/main/Common/cpp/Tools/ -#pragma once - -#include -#include -#include - -#include -#include - -using namespace facebook; -using namespace react; - -using KBJob = std::function; - -class KBJSScheduler { - public: - // With `jsCallInvoker`. - explicit KBJSScheduler( jsi::Runtime &rnRuntime, const std::shared_ptr &jsCallInvoker); - // With `runtimeExecutor`. - explicit KBJSScheduler( jsi::Runtime &rnRuntime, RuntimeExecutor runtimeExecutor); - const std::function scheduleOnJS = nullptr; - const std::shared_ptr getJSCallInvoker() const; - - protected: - jsi::Runtime &rnRuntime_; - RuntimeExecutor runtimeExecutor_ = nullptr; - const std::shared_ptr jsCallInvoker_ = nullptr; -}; diff --git a/rnmodules/react-native-kb/ios/Kb.h b/rnmodules/react-native-kb/ios/Kb.h index 8c919fbfa7dc..6a0c4ee019c1 100644 --- a/rnmodules/react-native-kb/ios/Kb.h +++ b/rnmodules/react-native-kb/ios/Kb.h @@ -8,7 +8,8 @@ #ifdef RCT_NEW_ARCH_ENABLED #import #import -@interface Kb : RCTEventEmitter +#import +@interface Kb : RCTEventEmitter @end #else #endif // RCT_NEW_ARCH_ENABLED diff --git a/rnmodules/react-native-kb/ios/Kb.mm b/rnmodules/react-native-kb/ios/Kb.mm index 6bb4193f51ea..d24371aff485 100644 --- a/rnmodules/react-native-kb/ios/Kb.mm +++ b/rnmodules/react-native-kb/ios/Kb.mm @@ -3,9 +3,6 @@ #import #import #import -#import -#import -#import #import #import #import @@ -15,7 +12,6 @@ #import #import #import -#import "./KBJSScheduler.h" #import "RNKbSpec.h" #import @@ -24,23 +20,6 @@ using namespace std; using namespace kb; -// used to keep track of objects getting destroyed on the js side -class KBTearDown : public jsi::HostObject { -public: - KBTearDown() { Tearup(); } - virtual ~KBTearDown() { - NSLog(@"KBTeardown!!!"); - Teardown(); - } - virtual jsi::Value get(jsi::Runtime &, const jsi::PropNameID &name) { - return jsi::Value::undefined(); - } - virtual void set(jsi::Runtime &, const jsi::PropNameID &name, const jsi::Value &value) {} - virtual std::vector getPropertyNames(jsi::Runtime &rt) { - return {}; - } -}; - @implementation FsPathsHolder @synthesize fsPaths; @@ -75,32 +54,14 @@ - (void)dealloc { static NSString *kbStoredDeviceToken = nil; static NSDictionary *kbInitialNotification = nil; -@interface RCTBridge (JSIRuntime) -- (void *)runtime; -@end - -@interface RCTBridge (RCTTurboModule) -- (std::shared_ptr)jsCallInvoker; -- (void)_tryAndHandleError:(dispatch_block_t)block; -@end - -@interface RCTBridge () -- (JSGlobalContextRef)jsContextRef; -- (void *)runtime; -- (void)dispatchBlock:(dispatch_block_t)block queue:(dispatch_queue_t)queue; -@end - @interface Kb () @property dispatch_queue_t readQueue; @end -@implementation Kb - -jsi::Runtime *_jsRuntime; -std::shared_ptr jsScheduler; - -// sanity check the runtime isn't out of sync due to reload etc -void *currentRuntime = nil; +@implementation Kb { + std::shared_ptr kbBridge_; + BOOL isInvalidated_; +} RCT_EXPORT_MODULE() @@ -111,6 +72,7 @@ + (BOOL)requiresMainQueueSetup { - (instancetype)init { self = [super init]; kbSharedInstance = self; + isInvalidated_ = NO; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleHardwareKeyPressed:) name:@"hardwareKeyPressed" @@ -161,12 +123,13 @@ + (void)handlePastedImages:(NSArray *)images { - (void)invalidate { [[NSNotificationCenter defaultCenter] removeObserver:self]; - currentRuntime = nil; - _jsRuntime = nil; + isInvalidated_ = YES; kbPasteImageEnabled = NO; + if (kbBridge_) { + kbBridge_->teardown(); + kbBridge_.reset(); + } [super invalidate]; - Teardown(); - self.bridge = nil; self.readQueue = nil; NSError *error = nil; KeybaseReset(&error); @@ -186,45 +149,30 @@ - (void)invalidate { return std::make_shared(params); } -- (void)sendToJS:(NSData *)data { - __weak __typeof__(self) weakSelf = self; - - jsScheduler->scheduleOnJS([data, weakSelf](jsi::Runtime &jsiRuntime) { - __typeof__(self) strongSelf = weakSelf; - if (!strongSelf) { - NSLog(@"Failed to find self in sendToJS invokeAsync!!!"); - return; - } - auto jsRuntimePtr = [strongSelf javaScriptRuntimePointer]; - if (!jsRuntimePtr) { - NSLog(@"Failed to find jsi in sendToJS invokeAsync!!!"); - return; - } - - int size = (int)[data length]; - if (size <= 0) { - NSLog(@"Invalid data size in sendToJS: %d", size); - return; - } - try { - auto values = PrepRpcOnJS(jsiRuntime, (uint8_t *)[data bytes], size); - RpcOnJS(jsiRuntime, values, [](const std::string &err) { - KeybaseLogToService([NSString - stringWithFormat:@"dNativeLogger: [%f,\"jsi rpconjs error: %@\"]", - [[NSDate date] timeIntervalSince1970] * 1000, - [NSString stringWithUTF8String:err.c_str()]]); +// RCTTurboModuleWithJSIBindings — called automatically by RN when the module loads +- (void)installJSIBindingsWithRuntime:(jsi::Runtime &)runtime + callInvoker:(const std::shared_ptr &)callInvoker { + kbBridge_ = std::make_shared(); + kbBridge_->install(runtime, callInvoker, + // writeToGo callback + [](void *ptr, size_t size) { + NSData *data = [NSData dataWithBytesNoCopy:ptr length:size freeWhenDone:NO]; + NSError *error = nil; + KeybaseWriteArr(data, &error); + if (error) { + NSLog(@"Error writing data: %@", error); + } + }, + // error callback + [](const std::string &err) { + KeybaseLogToService([NSString + stringWithFormat:@"dNativeLogger: [%f,\"jsi error: %s\"]", + [[NSDate date] timeIntervalSince1970] * 1000, + err.c_str()]); }); - } catch (const std::exception &e) { - NSLog(@"Exception in sendToJS msgpack processing: %s", e.what()); - KeybaseLogToService([NSString - stringWithFormat:@"dNativeLogger: [%f,\"sendToJS unknown exception\"]", - [[NSDate date] timeIntervalSince1970] * 1000]); - } - }); -} -- (jsi::Runtime *)javaScriptRuntimePointer { - return _jsRuntime; + KeybaseLogToService([NSString stringWithFormat:@"dNativeLogger: [%f,\"jsi install success (via installJSIBindings)\"]", + [[NSDate date] timeIntervalSince1970] * 1000]); } // from react-native-localize @@ -319,16 +267,15 @@ - (NSDictionary *)getConstants { } } +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(install) { + // No-op: JSI bindings are now installed via installJSIBindingsWithRuntime:callInvoker: + return @YES; +} + RCT_EXPORT_METHOD(notifyJSReady) { __weak __typeof__(self) weakSelf = self; dispatch_async(dispatch_get_main_queue(), ^{ - // Setup infrastructure - [[NSNotificationCenter defaultCenter] - addObserver:self - selector:@selector(engineReset) - name:RCTJavaScriptWillStartLoadingNotification - object:nil]; self.readQueue = dispatch_queue_create("go_bridge_queue_read", DISPATCH_QUEUE_SERIAL); // Signal to Go that JS is ready @@ -340,8 +287,8 @@ - (NSDictionary *)getConstants { while (true) { { __typeof__(self) strongSelf = weakSelf; - if (!strongSelf || !strongSelf.bridge) { - NSLog(@"Bridge dead, bailing from ReadArr loop"); + if (!strongSelf || strongSelf->isInvalidated_) { + NSLog(@"Module invalidated, bailing from ReadArr loop"); return; } } @@ -352,8 +299,8 @@ - (NSDictionary *)getConstants { NSLog(@"Error reading data: %@", error); } else if (data) { __typeof__(self) strongSelf = weakSelf; - if (strongSelf) { - [strongSelf sendToJS:data]; + if (strongSelf && strongSelf->kbBridge_) { + strongSelf->kbBridge_->onDataFromGo((uint8_t *)[data bytes], (int)[data length]); } } } @@ -363,36 +310,6 @@ - (NSDictionary *)getConstants { @synthesize callInvoker = _callInvoker; -RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(install) { - RCTCxxBridge *cxxBridge = (RCTCxxBridge *)self.bridge; - _jsRuntime = (jsi::Runtime *)cxxBridge.runtime; - auto &rnRuntime = *(jsi::Runtime *)cxxBridge.runtime; - jsScheduler = std::make_shared(rnRuntime, _callInvoker.callInvoker); - - // stash the current runtime to keep in sync - auto rpcOnGoWrap = [](Runtime &runtime, const Value &thisValue, const Value *arguments, size_t count) -> Value { - return RpcOnGo(runtime, thisValue, arguments, count, [](void *ptr, size_t size) { - NSData *result = [NSData dataWithBytesNoCopy:ptr length:size freeWhenDone:NO]; - NSError *error = nil; - KeybaseWriteArr(result, &error); - if (error) { - NSLog(@"Error writing data: %@", error); - } - }); - }; - - KeybaseLogToService([NSString stringWithFormat:@"dNativeLogger: [%f,\"jsi install success\"]", - [[NSDate date] timeIntervalSince1970] * 1000]); - - _jsRuntime->global().setProperty(*_jsRuntime, "rpcOnGo", - Function::createFromHostFunction(*_jsRuntime, PropNameID::forAscii(*_jsRuntime, "rpcOnGo"), 1, std::move(rpcOnGoWrap))); - - // register a global so we get notified when the runtime is killed so we can - // cleanup - _jsRuntime->global().setProperty(*_jsRuntime, "kbTeardown", jsi::Object::createFromHostObject(*_jsRuntime, std::make_shared())); - return @YES; -} - RCT_EXPORT_METHOD(getDefaultCountryCode : (RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock)reject) { @@ -489,33 +406,33 @@ - (NSDictionary *)getConstants { FsPathsHolder *holder = [FsPathsHolder sharedFsPathsHolder]; NSDictionary *fsPaths = holder.fsPaths; NSString *logFilePath = fsPaths[@"logFile"]; - + if (!logFilePath || logFilePath.length == 0) { resolve(@YES); return; } - + NSString *logDir = [logFilePath stringByDeletingLastPathComponent]; NSFileManager *fm = [NSFileManager defaultManager]; - + if (![fm fileExistsAtPath:logDir]) { resolve(@YES); return; } - + NSError *error = nil; NSArray *files = [fm contentsOfDirectoryAtPath:logDir error:&error]; - + if (error) { NSLog(@"Error listing log directory: %@", error.localizedDescription); resolve(@YES); return; } - + for (NSString *fileName in files) { NSString *filePath = [logDir stringByAppendingPathComponent:fileName]; NSFileHandle *fileHandle = [NSFileHandle fileHandleForWritingAtPath:filePath]; - + if (fileHandle) { @try { [fileHandle truncateFileAtOffset:0]; @@ -526,7 +443,7 @@ - (NSDictionary *)getConstants { } } } - + resolve(@YES); } diff --git a/rnmodules/react-native-kb/src/index.tsx b/rnmodules/react-native-kb/src/index.tsx index 964365198291..0db98f074070 100644 --- a/rnmodules/react-native-kb/src/index.tsx +++ b/rnmodules/react-native-kb/src/index.tsx @@ -1,25 +1,7 @@ -import {NativeModules, Platform, NativeEventEmitter} from 'react-native' +import {Platform, NativeEventEmitter} from 'react-native' +import KbNative from './NativeKb' -const LINKING_ERROR = - `The package 'react-native-kb' doesn't seem to be linked. Make sure: \n\n` + - Platform.select({ios: "- You have run 'pod install'\n", default: ''}) + - '- You rebuilt the app after installing the package\n' + - '- You are not using Expo Go\n' - -const isTurboModuleEnabled = global.__turboModuleProxy != null - -const KbModule = isTurboModuleEnabled ? require('./NativeKb').default : NativeModules['Kb'] - -const Kb = KbModule - ? KbModule - : new Proxy( - {}, - { - get() { - throw new Error(LINKING_ERROR) - }, - } - ) +const Kb = KbNative export const getDefaultCountryCode = (): Promise => { return Kb.getDefaultCountryCode() @@ -37,7 +19,7 @@ export const logSend = ( } export const install = () => { - Kb.install() + // No-op: JSI bindings are now installed automatically via TurboModuleWithJSIBindings } export const iosGetHasShownPushPrompt = (): Promise => { if (Platform.OS === 'ios') { From a94fbfc2dc2ce1d4f8efa17eab503f5ab93dedb5 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Wed, 25 Feb 2026 00:11:55 -0500 Subject: [PATCH 2/4] WIP --- .../react-native-kb/android/cpp-adapter.cpp | 129 ++++++------------ 1 file changed, 38 insertions(+), 91 deletions(-) diff --git a/rnmodules/react-native-kb/android/cpp-adapter.cpp b/rnmodules/react-native-kb/android/cpp-adapter.cpp index 5b77f7204890..b11e52b4e737 100644 --- a/rnmodules/react-native-kb/android/cpp-adapter.cpp +++ b/rnmodules/react-native-kb/android/cpp-adapter.cpp @@ -3,103 +3,56 @@ #include #include #include -#include #include -#include -#include using namespace facebook; using namespace facebook::jsi; using namespace facebook::react; -static std::shared_ptr g_bridge; -static JavaVM *java_vm = nullptr; -static jobject java_object = nullptr; -static jclass g_kbClass = nullptr; -static jmethodID g_rpcOnGoMethod = nullptr; +struct JKbModule : jni::JavaClass { + static constexpr auto kJavaDescriptor = "Lcom/reactnativekb/KbModule;"; +}; -/** - * A simple callback function that allows us to detach current JNI Environment - * when the thread is destroyed. - * See https://stackoverflow.com/a/30026231 for detailed explanation - */ -void DeferThreadDetach(JNIEnv *env) { - static pthread_key_t thread_key; +class KbNativeAdapter { +public: + jni::global_ref jModule_; + std::shared_ptr bridge_; - static auto run_once = [] { - const auto err = pthread_key_create(&thread_key, [](void *ts_env) { - if (ts_env) { - java_vm->DetachCurrentThread(); - } - }); - if (err) { - } - return 0; - }(); - static_cast(run_once); + explicit KbNativeAdapter(jni::alias_ref jModule) + : jModule_(jni::make_global(jModule)) {} - const auto ts_env = pthread_getspecific(thread_key); - if (!ts_env) { - if (pthread_setspecific(thread_key, env)) { - } + void writeToGo(void *ptr, size_t size) { + jni::ThreadScope scope; + auto env = jni::Environment::current(); + auto jba = env->NewByteArray(size); + env->SetByteArrayRegion(jba, 0, size, (jbyte *)ptr); + static auto method = + JKbModule::javaClassStatic() + ->getMethod)>("rpcOnGo"); + method(jModule_, jni::wrap_alias(jba)); + env->DeleteLocalRef(jba); } -} +}; -/** - * Get a JNIEnv* valid for this thread, regardless of whether - * we're on a native thread or a Java thread. - * See https://stackoverflow.com/a/30026231 for detailed explanation - */ -JNIEnv *GetJniEnv() { - JNIEnv *env = nullptr; - auto get_env_result = java_vm->GetEnv((void **)&env, JNI_VERSION_1_6); - if (get_env_result == JNI_EDETACHED) { - if (java_vm->AttachCurrentThread(&env, nullptr) == JNI_OK) { - DeferThreadDetach(env); - } - } - return env; -} - -static void cacheJNIMethods(JNIEnv *env) { - if (!g_kbClass && java_object) { - jclass cls = env->GetObjectClass(java_object); - g_kbClass = (jclass)env->NewGlobalRef(cls); - g_rpcOnGoMethod = env->GetMethodID(g_kbClass, "rpcOnGo", "([B)V"); - env->DeleteLocalRef(cls); - } -} +static std::shared_ptr g_adapter; static jni::local_ref -getBindingsInstaller(jni::alias_ref thiz) { - JNIEnv *env = jni::Environment::current(); - env->GetJavaVM(&java_vm); - if (java_object) { - env->DeleteGlobalRef(java_object); - } - java_object = env->NewGlobalRef(thiz.get()); - cacheJNIMethods(env); +getBindingsInstaller(jni::alias_ref thiz) { + g_adapter = std::make_shared(thiz); return BindingsInstallerHolder::newObjectCxxArgs( - [](jsi::Runtime &runtime, - const std::shared_ptr &callInvoker) { - if (g_bridge) { - g_bridge->teardown(); + [adapter = g_adapter](jsi::Runtime &runtime, + const std::shared_ptr &callInvoker) { + if (adapter->bridge_) { + adapter->bridge_->teardown(); } - g_bridge = std::make_shared(); - g_bridge->install( + adapter->bridge_ = std::make_shared(); + adapter->bridge_->install( runtime, callInvoker, - // writeToGo: called from JS thread when rpcOnGo fires - [](void *ptr, size_t size) { - JNIEnv *jniEnv = GetJniEnv(); - if (!jniEnv || !java_object || !g_rpcOnGoMethod) - return; - jbyteArray jba = jniEnv->NewByteArray(size); - jniEnv->SetByteArrayRegion(jba, 0, size, (jbyte *)ptr); - jniEnv->CallVoidMethod(java_object, g_rpcOnGoMethod, jba); - jniEnv->DeleteLocalRef(jba); + [weak = std::weak_ptr(adapter)](void *ptr, size_t size) { + if (auto a = weak.lock()) + a->writeToGo(ptr, size); }, - // onError [](const std::string &err) { __android_log_print(ANDROID_LOG_ERROR, "KBBridge", "JSI error: %s", err.c_str()); @@ -107,23 +60,17 @@ getBindingsInstaller(jni::alias_ref thiz) { }); } -static void nativeOnDataFromGo(jni::alias_ref thiz, +static void nativeOnDataFromGo(jni::alias_ref thiz, jni::alias_ref data) { - auto bridge = g_bridge; - if (!bridge || !data) + auto adapter = g_adapter; + if (!adapter || !adapter->bridge_ || !data) return; - JNIEnv *env = jni::Environment::current(); - auto rawArray = data.get(); - auto size = static_cast(env->GetArrayLength(rawArray)); - auto bytes = - reinterpret_cast(env->GetByteArrayElements(rawArray, nullptr)); - bridge->onDataFromGo(bytes, size); - env->ReleaseByteArrayElements(rawArray, reinterpret_cast(bytes), - JNI_ABORT); + auto pinned = data->pin(); + adapter->bridge_->onDataFromGo(reinterpret_cast(pinned.get()), + pinned.size()); } JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) { - java_vm = vm; return jni::initialize(vm, [] { jni::findClassStatic("com/reactnativekb/KbModule") ->registerNatives({ From fe8d26329434d46e15f897f51b472f21085541a6 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Wed, 25 Feb 2026 10:40:43 -0500 Subject: [PATCH 3/4] WIP --- .../react-native-kb/cpp/react-native-kb.cpp | 26 +++++++++++++------ .../react-native-kb/cpp/react-native-kb.h | 14 +++++++--- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/rnmodules/react-native-kb/cpp/react-native-kb.cpp b/rnmodules/react-native-kb/cpp/react-native-kb.cpp index 0624469f8688..2b652cfd8a9c 100644 --- a/rnmodules/react-native-kb/cpp/react-native-kb.cpp +++ b/rnmodules/react-native-kb/cpp/react-native-kb.cpp @@ -1,5 +1,6 @@ #include "react-native-kb.h" #include +#include #include using namespace facebook; @@ -7,6 +8,13 @@ using namespace facebook::jsi; namespace kb { +struct KBBridge::MsgpackState { + msgpack::unpacker unpacker; +}; + +KBBridge::KBBridge() = default; +KBBridge::~KBBridge() = default; + void KBBridge::teardown() { isTornDown_.store(true); // Clear cached JSI objects while the runtime is still alive. @@ -44,7 +52,8 @@ static std::string mpToString(msgpack::object &o) { } } -Value KBBridge::convertMPToJSI(Runtime &runtime, msgpack::object &o) { +Value KBBridge::convertMPToJSI(Runtime &runtime, void *mpObj) { + auto &o = *static_cast(mpObj); switch (o.type) { case msgpack::type::STR: return jsi::String::createFromUtf8(runtime, o.as()); @@ -68,7 +77,7 @@ Value KBBridge::convertMPToJSI(Runtime &runtime, msgpack::object &o) { auto *const pend = o.via.map.ptr + o.via.map.size; for (; p < pend; ++p) { auto key = mpToString(p->key); - auto val = convertMPToJSI(runtime, p->val); + auto val = convertMPToJSI(runtime, &p->val); obj.setProperty(runtime, jsi::String::createFromUtf8(runtime, key), val); } return obj; @@ -98,7 +107,7 @@ Value KBBridge::convertMPToJSI(Runtime &runtime, msgpack::object &o) { jsi::Array arr(runtime, size); for (uint32_t i = 0; i < size; ++i) { arr.setValueAtIndex(runtime, i, - convertMPToJSI(runtime, o.via.array.ptr[i])); + convertMPToJSI(runtime, &o.via.array.ptr[i])); } return arr; } @@ -114,6 +123,7 @@ void KBBridge::install( std::function onError) { callInvoker_ = std::move(callInvoker); onError_ = std::move(onError); + mp_ = std::make_unique(); auto rpcOnGo = Function::createFromHostFunction( runtime, PropNameID::forAscii(runtime, "rpcOnGo"), 1, @@ -175,12 +185,12 @@ void KBBridge::onDataFromGo(uint8_t *data, int size) { try { auto values = std::make_shared>(); - unpacker_.reserve_buffer(size); - std::copy(data, data + size, unpacker_.buffer()); - unpacker_.buffer_consumed(size); + mp_->unpacker.reserve_buffer(size); + std::copy(data, data + size, mp_->unpacker.buffer()); + mp_->unpacker.buffer_consumed(size); while (true) { msgpack::object_handle result; - if (unpacker_.next(result)) { + if (mp_->unpacker.next(result)) { if (readState_ == ReadState::needSize) { readState_ = ReadState::needContent; } else { @@ -220,7 +230,7 @@ void KBBridge::onDataFromGo(uint8_t *data, int size) { for (auto &result : *values) { msgpack::object obj(result.get()); - Value value = self->convertMPToJSI(runtime, obj); + Value value = self->convertMPToJSI(runtime, &obj); if (self->isTornDown_.load()) { return; } diff --git a/rnmodules/react-native-kb/cpp/react-native-kb.h b/rnmodules/react-native-kb/cpp/react-native-kb.h index c92a3ec9eec0..2197bcdd3b9a 100644 --- a/rnmodules/react-native-kb/cpp/react-native-kb.h +++ b/rnmodules/react-native-kb/cpp/react-native-kb.h @@ -6,14 +6,18 @@ #include #include #include -#include -#include #include namespace kb { class KBBridge : public std::enable_shared_from_this { public: + KBBridge(); + ~KBBridge(); + KBBridge(const KBBridge &) = delete; + KBBridge &operator=(const KBBridge &) = delete; + KBBridge(KBBridge &&) = delete; + KBBridge &operator=(KBBridge &&) = delete; void install(facebook::jsi::Runtime &runtime, std::shared_ptr callInvoker, std::function writeToGo, @@ -30,7 +34,9 @@ class KBBridge : public std::enable_shared_from_this { enum class ReadState { needSize, needContent }; ReadState readState_ = ReadState::needSize; - msgpack::unpacker unpacker_; + + struct MsgpackState; + std::unique_ptr mp_; std::unique_ptr cachedUint8ArrayCtor_; std::unique_ptr cachedRpcOnJs_; @@ -38,7 +44,7 @@ class KBBridge : public std::enable_shared_from_this { void resetCaches(facebook::jsi::Runtime &runtime); facebook::jsi::Value convertMPToJSI(facebook::jsi::Runtime &runtime, - msgpack::object &o); + void *mpObj); }; } // namespace kb From 9663737aa30979cb526246f812587ecbe7e6cd12 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Wed, 25 Feb 2026 16:58:30 -0500 Subject: [PATCH 4/4] native level cleanup (#28942) * encode msgpack on cpp side. rpc on just handle arrays. --- .../react-native-kb/android/CMakeLists.txt | 2 +- .../com/reactnativekb/DarkModePrefHelper.kt | 2 - .../com/reactnativekb/DarkModePreference.kt | 2 - .../main/java/com/reactnativekb/GuiConfig.kt | 14 +- .../main/java/com/reactnativekb/KbModule.kt | 271 ++++-------------- .../main/java/com/reactnativekb/KbPackage.kt | 13 +- .../java/com/reactnativekb/NativeLogger.kt | 7 +- .../java/com/reactnativekb/PathResolver.kt | 92 +++--- .../com/reactnativekb/ReadFileAsString.kt | 29 +- .../react-native-kb/cpp/react-native-kb.cpp | 146 +++++++++- .../react-native-kb/cpp/react-native-kb.h | 5 + rnmodules/react-native-kb/ios/Kb.mm | 20 +- .../react-native-kb/react-native-kb.podspec | 6 +- .../ossifrage/ChatBroadcastReceiver.kt | 44 ++- .../CustomBitmapMemoryCacheParamsSupplier.kt | 11 +- .../ossifrage/KBInstallReferrerListener.kt | 8 +- .../io/keybase/ossifrage/KBPushNotifier.kt | 29 +- .../io/keybase/ossifrage/KBReactPackage.kt | 5 +- .../java/io/keybase/ossifrage/KeyStore.kt | 15 +- .../KeybasePushNotificationListenerService.kt | 17 +- .../java/io/keybase/ossifrage/MainActivity.kt | 28 +- .../io/keybase/ossifrage/MainApplication.kt | 10 +- .../ossifrage/keystore/KeyStoreHelper.kt | 39 +-- .../keybase/ossifrage/modules/NativeLogger.kt | 2 +- .../ossifrage/modules/StorybookConstants.kt | 4 +- .../keybase/ossifrage/util/DeviceLockType.kt | 39 +-- shared/desktop/yarn-helper/index.tsx | 4 +- shared/engine/index.platform.native.tsx | 20 +- shared/globals.d.ts | 4 +- shared/ios/Keybase/AppDelegate.swift | 136 +++++---- shared/ios/Keybase/Fs.swift | 79 ++--- shared/ios/Keybase/Info.plist | 4 + shared/ios/Keybase/PerfFPSMonitor.swift | 7 +- shared/ios/Keybase/Pusher.swift | 9 +- .../ios/Keybase/ShareIntentDonatorImpl.swift | 17 +- .../KeybaseShare/ShareViewController.swift | 23 +- shared/ios/Podfile.lock | 2 +- shared/package.json | 1 + 38 files changed, 504 insertions(+), 662 deletions(-) diff --git a/rnmodules/react-native-kb/android/CMakeLists.txt b/rnmodules/react-native-kb/android/CMakeLists.txt index 37372ea7a10a..92cacd494863 100644 --- a/rnmodules/react-native-kb/android/CMakeLists.txt +++ b/rnmodules/react-native-kb/android/CMakeLists.txt @@ -16,7 +16,7 @@ message(INFO "params: ${NODE_MODULES_DIR}") # Specifies a path to native header files. include_directories( ../cpp - "${NODE_MODULES_DIR}/msgpack-cxx-6.1.0/include" + "${NODE_MODULES_DIR}/msgpack-cxx-7.0.0/include" ) set_target_properties( diff --git a/rnmodules/react-native-kb/android/src/main/java/com/reactnativekb/DarkModePrefHelper.kt b/rnmodules/react-native-kb/android/src/main/java/com/reactnativekb/DarkModePrefHelper.kt index 192bc517d23a..1c76672c567f 100644 --- a/rnmodules/react-native-kb/android/src/main/java/com/reactnativekb/DarkModePrefHelper.kt +++ b/rnmodules/react-native-kb/android/src/main/java/com/reactnativekb/DarkModePrefHelper.kt @@ -1,7 +1,5 @@ package com.reactnativekb -import kotlin.Throws - object DarkModePrefHelper { fun fromString(prefString: String): DarkModePreference { return when (prefString) { diff --git a/rnmodules/react-native-kb/android/src/main/java/com/reactnativekb/DarkModePreference.kt b/rnmodules/react-native-kb/android/src/main/java/com/reactnativekb/DarkModePreference.kt index b62b89766471..c318913c62cd 100644 --- a/rnmodules/react-native-kb/android/src/main/java/com/reactnativekb/DarkModePreference.kt +++ b/rnmodules/react-native-kb/android/src/main/java/com/reactnativekb/DarkModePreference.kt @@ -1,7 +1,5 @@ package com.reactnativekb -import kotlin.Throws - enum class DarkModePreference { System, AlwaysDark, diff --git a/rnmodules/react-native-kb/android/src/main/java/com/reactnativekb/GuiConfig.kt b/rnmodules/react-native-kb/android/src/main/java/com/reactnativekb/GuiConfig.kt index c78a5fd2ebcb..0544d0295070 100644 --- a/rnmodules/react-native-kb/android/src/main/java/com/reactnativekb/GuiConfig.kt +++ b/rnmodules/react-native-kb/android/src/main/java/com/reactnativekb/GuiConfig.kt @@ -1,27 +1,19 @@ package com.reactnativekb -import androidx.annotation.Nullable - import org.json.JSONException import org.json.JSONObject import java.io.File -class GuiConfig private constructor(filesDir: File?) { - private val filesDir: File? - - init { - this.filesDir = filesDir - } - +class GuiConfig private constructor(private val filesDir: File?) { fun asString(): String? { val filePath = File(filesDir, "/.config/keybase/gui_config.json") - return ReadFileAsString.read(filePath.getAbsolutePath()) + return ReadFileAsString.read(filePath.absolutePath) } fun getDarkMode(): DarkModePreference { return try { - val jsonObject = JSONObject(asString()) + val jsonObject = JSONObject(asString() ?: return DarkModePreference.System) val jsonObjectUI: JSONObject = jsonObject.getJSONObject("ui") val darkModeString: String = jsonObjectUI.getString("darkMode") DarkModePrefHelper.fromString(darkModeString) diff --git a/rnmodules/react-native-kb/android/src/main/java/com/reactnativekb/KbModule.kt b/rnmodules/react-native-kb/android/src/main/java/com/reactnativekb/KbModule.kt index 1d1d1dc053d0..4e66357aa29a 100644 --- a/rnmodules/react-native-kb/android/src/main/java/com/reactnativekb/KbModule.kt +++ b/rnmodules/react-native-kb/android/src/main/java/com/reactnativekb/KbModule.kt @@ -36,8 +36,6 @@ import com.facebook.react.module.annotations.ReactModule import com.facebook.react.turbomodule.core.interfaces.TurboModuleWithJSIBindings import com.facebook.react.turbomodule.core.interfaces.BindingsInstallerHolder import com.facebook.proguard.annotations.DoNotStrip -import com.google.android.gms.tasks.OnCompleteListener -import com.google.android.gms.tasks.Task import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.FirebaseMessaging import com.google.firebase.FirebaseApp @@ -56,8 +54,6 @@ import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicReference -import java.util.regex.Matcher -import java.util.regex.Pattern import keybase.Keybase import me.leolin.shortcutbadger.ShortcutBadger import keybase.Keybase.readArr @@ -113,170 +109,6 @@ class KbModule(reactContext: ReactApplicationContext?) : KbSpec(reactContext), T // not used } - /* - @ReactMethod - override fun processVideo(path: String, promise: Promise) { - Executors.newSingleThreadExecutor().execute { - try { - val inputFile = File(path) - if (!inputFile.exists()) { - promise.reject("FILE_NOT_FOUND", "Video file not found: $path") - return@execute - } - - val retriever = MediaMetadataRetriever() - try { - retriever.setDataSource(path) - val widthStr = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH) - val heightStr = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT) - val fileSize = inputFile.length() - - val width = widthStr?.toIntOrNull() ?: 0 - val height = heightStr?.toIntOrNull() ?: 0 - val maxPixels = 1920 * 1080 - val maxFileSize = 50L * 1024 * 1024 // 50MB - val pixelCount = width * height - - val needsCompression = pixelCount > maxPixels || fileSize > maxFileSize - NativeLogger.info("Video processing: width=$width, height=$height, pixelCount=$pixelCount, fileSize=$fileSize, needsCompression=$needsCompression") - - if (!needsCompression) { - Log.i("VideoCompression", "Video does not need compression, returning original path") - promise.resolve(path) - return@execute - } - - val outputFile = File(inputFile.parent, "${inputFile.nameWithoutExtension}.processed.mp4") - NativeLogger.info("Starting video compression: $path -> ${outputFile.absolutePath}") - compressVideo(path, outputFile.absolutePath, width, height, maxPixels) - - // Verify output file exists and is valid before resolving - if (!outputFile.exists()) { - throw IllegalStateException("Compressed video file does not exist: ${outputFile.absolutePath}") - } - val outputSize = outputFile.length() - if (outputSize == 0L) { - throw IllegalStateException("Compressed video file is empty: ${outputFile.absolutePath}") - } - NativeLogger.info("Video compression completed successfully: ${outputFile.absolutePath}, size=$outputSize bytes (original=${inputFile.length()} bytes)") - promise.resolve(outputFile.absolutePath) - } finally { - retriever.release() - } - } catch (e: Exception) { - NativeLogger.error("Error compressing video", e) - promise.reject("COMPRESSION_ERROR", "Failed to compress video: ${e.message}", e) - } - } - } - */ - - /* - private fun compressVideo(inputPath: String, outputPath: String, originalWidth: Int, originalHeight: Int, maxPixels: Int) { - val (outputWidth, outputHeight) = calculateOutputDimensions(originalWidth, originalHeight, maxPixels) - val targetBitrate = calculateBitrate(outputWidth, outputHeight) - - // Ensure output directory exists - val outputFile = File(outputPath) - outputFile.parentFile?.mkdirs() - - // Use Media3 Transformer for simple, reliable transcoding - // Note: Bitrate is controlled by the encoder automatically based on resolution - // Media3 Transformer doesn't expose direct bitrate control in TransformationRequest - // Transformer and all Media3 objects must be created and used on a thread with a Looper (main thread) - val latch = CountDownLatch(1) - val exceptionRef = AtomicReference(null) - val mainHandler = Handler(Looper.getMainLooper()) - - mainHandler.post { - try { - NativeLogger.info("compressVideo: Creating Media3 objects on main thread") - // Create file URI properly - val inputFile = File(inputPath) - val inputUri = Uri.fromFile(inputFile) - val mediaItem = MediaItem.fromUri(inputUri) - - // Apply scaling transformation if needed - val editedMediaItemBuilder = EditedMediaItem.Builder(mediaItem) - if (outputWidth != originalWidth || outputHeight != originalHeight) { - val scaleX = outputWidth.toFloat() / originalWidth.toFloat() - val scaleY = outputHeight.toFloat() / originalHeight.toFloat() - NativeLogger.info("compressVideo: Scaling from ${originalWidth}x${originalHeight} to ${outputWidth}x${outputHeight} (scale=$scaleX,$scaleY)") - val scaleTransformation = ScaleAndRotateTransformation.Builder() - .setScale(scaleX, scaleY) - .build() - // Effects class wraps video effects and audio processors - // Constructor takes (audioProcessors, videoEffects) as positional parameters - editedMediaItemBuilder.setEffects( - Effects(listOf(), listOf(scaleTransformation)) - ) - } - val editedMediaItem = editedMediaItemBuilder.build() - - // Set video encoder settings with target bitrate to actually compress the video - val videoEncoderSettings = VideoEncoderSettings.Builder() - .setBitrate(targetBitrate) - .build() - - // Create encoder factory with video encoder settings - val encoderFactory = DefaultEncoderFactory.Builder(reactContext) - .setRequestedVideoEncoderSettings(videoEncoderSettings) - .build() - - val transformationRequest = TransformationRequest.Builder() - .setVideoMimeType(MimeTypes.VIDEO_H264) - .setAudioMimeType(MimeTypes.AUDIO_AAC) - .build() - - NativeLogger.info("compressVideo: Creating Transformer with listener") - val transformer = Transformer.Builder(reactContext) - .setTransformationRequest(transformationRequest) - .setEncoderFactory(encoderFactory) - .addListener(object : Listener { - override fun onCompleted(composition: Composition, result: ExportResult) { - NativeLogger.info("compressVideo: Transformation completed successfully") - latch.countDown() - } - - override fun onError(composition: Composition, result: ExportResult, exception: ExportException) { - NativeLogger.error("compressVideo: Transformation error", exception) - exceptionRef.set(exception) - latch.countDown() - } - }) - .build() - - NativeLogger.info("compressVideo: Starting Transformer.start() (asynchronous)") - // Transformer.start() is asynchronous - completion is signaled via Listener callbacks - transformer.start(editedMediaItem, outputPath) - } catch (e: Exception) { - NativeLogger.error("Error in compressVideo Transformer operation", e) - exceptionRef.set(e) - latch.countDown() - } - } - - // Wait for Transformer operation to complete on main thread - // The Listener callbacks will signal completion via latch.countDown() - latch.await() - - // Check if an exception occurred during transformation - val exception = exceptionRef.get() - if (exception != null) { - throw exception - } - - // Validate that output file was created and is valid - if (!outputFile.exists()) { - throw IllegalStateException("Compressed video file was not created: $outputPath") - } - if (outputFile.length() == 0L) { - throw IllegalStateException("Compressed video file is empty: $outputPath") - } - NativeLogger.info("compressVideo: Output file validated - size=${outputFile.length()} bytes") - } - */ - private fun calculateOutputDimensions(width: Int, height: Int, maxPixels: Int): Pair { val pixelCount = width * height if (pixelCount <= maxPixels) { @@ -309,7 +141,7 @@ class KbModule(reactContext: ReactApplicationContext?) : KbSpec(reactContext), T */ private fun getBuildConfigValue(fieldName: String): Any? { try { - val clazz: Class<*> = Class.forName(reactContext.getPackageName() + ".BuildConfig") + val clazz: Class<*> = Class.forName("${reactContext.packageName}.BuildConfig") val field = clazz.getField(fieldName) return field.get(null) } catch (e: ClassNotFoundException) { @@ -323,7 +155,7 @@ class KbModule(reactContext: ReactApplicationContext?) : KbSpec(reactContext), T } private fun readGuiConfig(): String? { - return GuiConfig.getInstance(reactContext.getFilesDir())?.asString() + return GuiConfig.getInstance(reactContext.filesDir)?.asString() } @ReactMethod(isBlockingSynchronousMethod = true) @@ -333,28 +165,28 @@ class KbModule(reactContext: ReactApplicationContext?) : KbSpec(reactContext), T var isDeviceSecure = false try { val keyguardManager: KeyguardManager = reactContext.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager - isDeviceSecure = keyguardManager.isKeyguardSecure() + isDeviceSecure = keyguardManager.isKeyguardSecure } catch (e: Exception) { NativeLogger.warn(": Error reading keyguard secure state", e) } var serverConfig = "" try { - serverConfig = ReadFileAsString.read(reactContext.getCacheDir().getAbsolutePath() + "/Keybase/keybase.app.serverConfig") + serverConfig = ReadFileAsString.read("${reactContext.cacheDir.absolutePath}/Keybase/keybase.app.serverConfig") } catch (e: Exception) { NativeLogger.warn(": Error reading server config", e) } var cacheDir = "" run { - val dir: File? = reactContext.getCacheDir() + val dir: File? = reactContext.cacheDir if (dir != null) { - cacheDir = dir.getAbsolutePath() + cacheDir = dir.absolutePath } } var downloadDir = "" run { val dir: File? = reactContext.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS) if (dir != null) { - downloadDir = dir.getAbsolutePath() + downloadDir = dir.absolutePath } } @@ -378,7 +210,7 @@ class KbModule(reactContext: ReactApplicationContext?) : KbSpec(reactContext), T override fun getDefaultCountryCode(promise: Promise) { try { val tm: TelephonyManager = reactContext.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager - val countryCode: String = tm.getNetworkCountryIso() + val countryCode: String = tm.networkCountryIso promise.resolve(countryCode) } catch (e: Exception) { promise.reject(e) @@ -404,7 +236,7 @@ class KbModule(reactContext: ReactApplicationContext?) : KbSpec(reactContext), T override fun androidOpenSettings() { val intent = Intent() intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) - val uri: Uri = Uri.fromParts("package", reactContext.getPackageName(), null) + val uri: Uri = Uri.fromParts("package", reactContext.packageName, null) intent.setData(uri) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) reactContext.startActivity(intent) @@ -429,19 +261,16 @@ class KbModule(reactContext: ReactApplicationContext?) : KbSpec(reactContext), T private fun setSecureFlag() { val prefs: SharedPreferences = reactContext.getSharedPreferences("SecureFlag", Context.MODE_PRIVATE) val setSecure: Boolean = prefs.getBoolean("setSecure", !misTestDevice) - val activity: Activity? = reactContext.getCurrentActivity() + val activity: Activity? = reactContext.currentActivity if (activity != null) { - activity.runOnUiThread(object : Runnable { - @Override - override fun run() { - val window: Window = activity.getWindow() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH && setSecure) { - window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) - } else { - window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) - } + activity.runOnUiThread { + val window: Window = activity.window + if (setSecure) { + window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) + } else { + window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) } - }) + } } } @@ -449,12 +278,13 @@ class KbModule(reactContext: ReactApplicationContext?) : KbSpec(reactContext), T @ReactMethod override fun shareListenersRegistered() { try { - val activity: Activity? = reactContext.getCurrentActivity() + val activity: Activity? = reactContext.currentActivity if (activity != null) { val m: Method = activity.javaClass.getMethod("shareListenersRegistered") m.invoke(activity) } } catch (ex: Exception) { + NativeLogger.warn("Error calling shareListenersRegistered", ex) } } @@ -498,12 +328,12 @@ class KbModule(reactContext: ReactApplicationContext?) : KbSpec(reactContext), T private fun handleNonTextFileSharing(file: File, intent: Intent, promise: Promise) { try { // note in JS initPlatformSpecific changes the cache dir so this works - val fileUri: Uri = FileProvider.getUriForFile(reactContext, reactContext.getPackageName() + ".fileprovider", file) + val fileUri: Uri = FileProvider.getUriForFile(reactContext, "${reactContext.packageName}.fileprovider", file) intent.putExtra(Intent.EXTRA_STREAM, fileUri) intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) startSharing(intent, promise) } catch (ex: Exception) { - promise.reject(Error("Error sharing file " + ex.getLocalizedMessage())) + promise.reject(Error("Error sharing file ${ex.localizedMessage}")) } } @@ -552,28 +382,28 @@ class KbModule(reactContext: ReactApplicationContext?) : KbSpec(reactContext), T override fun getRegistrationToken(promise: Promise) { ensureFirebase() FirebaseMessaging.getInstance().getToken() - .addOnCompleteListener(OnCompleteListener { task -> - if (!task.isSuccessful()) { - NativeLogger.info("Fetching FCM registration token failed " + task.getException()) - promise.reject("Fetching FCM registration token failed") - return@OnCompleteListener + .addOnCompleteListener { task -> + if (!task.isSuccessful) { + NativeLogger.info("Fetching FCM registration token failed ${task.exception}") + promise.reject("E_FCM_TOKEN", "Fetching FCM registration token failed") + return@addOnCompleteListener } // Get new FCM registration token val token: String? = task.result if (token == null) { - promise.reject("null token") - return@OnCompleteListener + promise.reject("E_FCM_TOKEN", "null token") + return@addOnCompleteListener } NativeLogger.info("Got token: $token") promise.resolve(token) - }) + } } // Unlink @Throws(IOException::class) private fun deleteRecursive(fileOrDirectory: File) { - if (fileOrDirectory.isDirectory()) { + if (fileOrDirectory.isDirectory) { val files = fileOrDirectory.listFiles() if (files == null) { throw NullPointerException("Received null trying to list files of directory '$fileOrDirectory'") @@ -595,16 +425,13 @@ class KbModule(reactContext: ReactApplicationContext?) : KbSpec(reactContext), T misTestDevice = isTestDevice(reactContext) setSecureFlag() reactContext.addLifecycleEventListener(object : LifecycleEventListener { - @Override override fun onHostResume() { setSecureFlag() } - @Override override fun onHostPause() { } - @Override override fun onHostDestroy() { } }) @@ -636,7 +463,7 @@ class KbModule(reactContext: ReactApplicationContext?) : KbSpec(reactContext), T deleteRecursive(File(normalizedPath)) promise.resolve(true) } catch (err: Exception) { - promise.reject("EUNSPECIFIED", err.getLocalizedMessage()) + promise.reject("EUNSPECIFIED", err.localizedMessage) } } @@ -648,20 +475,20 @@ class KbModule(reactContext: ReactApplicationContext?) : KbSpec(reactContext), T val stat: WritableMap = Arguments.createMap() if (isAsset(path)) { val name: String = path.replace(FILE_PREFIX_BUNDLE_ASSET, "") - val fd: AssetFileDescriptor = reactContext.getAssets().openFd(name) + val fd: AssetFileDescriptor = reactContext.assets.openFd(name) stat.putString("filename", name) stat.putString("path", path) stat.putString("type", "asset") - stat.putString("size", fd.getLength().toString()) + stat.putString("size", fd.length.toString()) stat.putInt("lastModified", 0) } else { val target = File(path) if (!target.exists()) { return null } - stat.putString("filename", target.getName()) - stat.putString("path", target.getPath()) - stat.putString("type", if (target.isDirectory()) "directory" else "file") + stat.putString("filename", target.name) + stat.putString("path", target.path) + stat.putString("type", if (target.isDirectory) "directory" else "file") stat.putString("size", target.length().toString()) val lastModified: String = target.lastModified().toString() stat.putString("lastModified", lastModified) @@ -694,6 +521,7 @@ class KbModule(reactContext: ReactApplicationContext?) : KbSpec(reactContext), T size = sizeStr.toLong() } } + @Suppress("DEPRECATION") dm.addCompletedDownload( if (config.hasKey("title")) config.getString("title") else "", if (config.hasKey("description")) config.getString("description") else "", @@ -705,7 +533,7 @@ class KbModule(reactContext: ReactApplicationContext?) : KbSpec(reactContext), T ) promise.resolve(null) } catch (ex: Exception) { - promise.reject("EUNSPECIFIED", ex.getLocalizedMessage()) + promise.reject("EUNSPECIFIED", ex.localizedMessage) } } @@ -714,13 +542,14 @@ class KbModule(reactContext: ReactApplicationContext?) : KbSpec(reactContext), T @ReactMethod override fun androidAppColorSchemeChanged(prefString: String) { try { - val activity: Activity? = reactContext.getCurrentActivity() + val activity: Activity? = reactContext.currentActivity if (activity != null) { val m: Method = activity.javaClass.getMethod("setBackgroundColor", DarkModePreference::class.java) val pref: DarkModePreference = DarkModePrefHelper.fromString(prefString) m.invoke(activity, pref) } } catch (ex: Exception) { + NativeLogger.warn("Error calling androidAppColorSchemeChanged", ex) } } @@ -837,7 +666,6 @@ class KbModule(reactContext: ReactApplicationContext?) : KbSpec(reactContext), T init { this.reactContext = reactContext reactContext.addLifecycleEventListener(object : LifecycleEventListener { - @Override override fun onHostResume() { if (executor == null) { val ex = Executors.newSingleThreadExecutor() @@ -846,22 +674,19 @@ class KbModule(reactContext: ReactApplicationContext?) : KbSpec(reactContext), T } } - @Override override fun onHostPause() { } - @Override override fun onHostDestroy() { destroy() } }) } - @Override override fun run() { do { try { - Thread.currentThread().setName("ReadFromKBLib") + Thread.currentThread().name = "ReadFromKBLib" val data: ByteArray = readArr() if (!reactContext.hasActiveReactInstance()) { NativeLogger.info("$NAME: JS Bridge is dead, dropping engine message") @@ -875,7 +700,7 @@ class KbModule(reactContext: ReactApplicationContext?) : KbSpec(reactContext), T NativeLogger.error("Exception in ReadFromKBLib.run", e) } } - } while (!Thread.currentThread().isInterrupted() && reactContext.hasActiveReactInstance()) + } while (!Thread.currentThread().isInterrupted && reactContext.hasActiveReactInstance()) } } @@ -892,7 +717,7 @@ class KbModule(reactContext: ReactApplicationContext?) : KbSpec(reactContext), T // We often hit this timeout during app resume, e.g. hit the back // button to go to home screen and then tap Keybase app icon again. if (executor?.awaitTermination(3, TimeUnit.SECONDS)== false) { - NativeLogger.warn(NAME.toString() + ": Executor pool didn't shut down cleanly") + NativeLogger.warn("$NAME: Executor pool didn't shut down cleanly") } executor = null } catch (e: Exception) { @@ -926,9 +751,9 @@ class KbModule(reactContext: ReactApplicationContext?) : KbSpec(reactContext), T } const val NAME: String = "Kb" - private val RN_NAME: String = "ReactNativeJS" - private val RPC_META_EVENT_NAME: String = "kb-meta-engine-event" - private val RPC_META_EVENT_ENGINE_RESET: String = "kb-engine-reset" + private const val RN_NAME: String = "ReactNativeJS" + private const val RPC_META_EVENT_NAME: String = "kb-meta-engine-event" + private const val RPC_META_EVENT_ENGINE_RESET: String = "kb-engine-reset" private const val MAX_TEXT_FILE_SIZE = 100 * 1024 // 100 kiB private val LINE_SEPARATOR: String? = System.getProperty("line.separator") private const val HW_KEY_EVENT: String = "hardwareKeyPressed" @@ -968,12 +793,12 @@ class KbModule(reactContext: ReactApplicationContext?) : KbSpec(reactContext), T return "true".equals(testLabSetting) } - private val FILE_PREFIX_BUNDLE_ASSET: String = "bundle-assets://" + private const val FILE_PREFIX_BUNDLE_ASSET: String = "bundle-assets://" // engine private fun relayReset(reactContext: ReactApplicationContext) { if (!reactContext.hasActiveReactInstance()) { - NativeLogger.info(NAME.toString() + ": JS Bridge is dead, Can't send EOF message") + NativeLogger.info("$NAME: JS Bridge is dead, Can't send EOF message") } else { reactContext.emitDeviceEvent(RPC_META_EVENT_NAME, RPC_META_EVENT_ENGINE_RESET) } diff --git a/rnmodules/react-native-kb/android/src/main/java/com/reactnativekb/KbPackage.kt b/rnmodules/react-native-kb/android/src/main/java/com/reactnativekb/KbPackage.kt index 021c99ae8cf0..8f88320027f6 100644 --- a/rnmodules/react-native-kb/android/src/main/java/com/reactnativekb/KbPackage.kt +++ b/rnmodules/react-native-kb/android/src/main/java/com/reactnativekb/KbPackage.kt @@ -1,18 +1,12 @@ package com.reactnativekb -import androidx.annotation.Nullable - import com.facebook.react.bridge.NativeModule import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.module.model.ReactModuleInfo import com.facebook.react.module.model.ReactModuleInfoProvider -import com.facebook.react.TurboReactPackage - -import java.util.HashMap -import java.util.Map +import com.facebook.react.BaseReactPackage -class KbPackage : TurboReactPackage() { - @Nullable +class KbPackage : BaseReactPackage() { override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? { return if (name == KbModule.NAME) { KbModule(reactContext) @@ -23,12 +17,11 @@ class KbPackage : TurboReactPackage() { override fun getReactModuleInfoProvider(): ReactModuleInfoProvider { return ReactModuleInfoProvider { - val moduleInfos: MutableMap = HashMap() + val moduleInfos: MutableMap = mutableMapOf() val isTurboModule = true moduleInfos[KbModule.NAME] = ReactModuleInfo( KbModule.NAME, KbModule.NAME, - false, // canOverrideExistingModule false, // needsEagerInit true, // hasConstants false, // isCxxModule diff --git a/rnmodules/react-native-kb/android/src/main/java/com/reactnativekb/NativeLogger.kt b/rnmodules/react-native-kb/android/src/main/java/com/reactnativekb/NativeLogger.kt index 17852364de2d..ed81980d427e 100644 --- a/rnmodules/react-native-kb/android/src/main/java/com/reactnativekb/NativeLogger.kt +++ b/rnmodules/react-native-kb/android/src/main/java/com/reactnativekb/NativeLogger.kt @@ -12,21 +12,20 @@ import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.WritableArray class NativeLogger(reactContext: ReactApplicationContext?) : ReactContextBaseJavaModule(reactContext) { - @Override override fun getName(): String { return NAME } companion object { - private val NAME: String = "NativeLogger" - private val RN_NAME: String = "ReactNativeJS" + private const val NAME: String = "NativeLogger" + private const val RN_NAME: String = "ReactNativeJS" fun rawLog(tag: String, jsonLog: String) { Log.i(tag + NAME, jsonLog) } private fun formatLine(tagPrefix: String, toLog: String): String { // Copies the Style JS outputs in native/logger.native.tsx - return tagPrefix + NAME + ": [" + System.currentTimeMillis() + ",\"" + toLog + "\"]" + return "${tagPrefix}${NAME}: [${System.currentTimeMillis()},\"$toLog\"]" } fun error(log: String) { diff --git a/rnmodules/react-native-kb/android/src/main/java/com/reactnativekb/PathResolver.kt b/rnmodules/react-native-kb/android/src/main/java/com/reactnativekb/PathResolver.kt index 1c907d246b15..3d81c772d932 100644 --- a/rnmodules/react-native-kb/android/src/main/java/com/reactnativekb/PathResolver.kt +++ b/rnmodules/react-native-kb/android/src/main/java/com/reactnativekb/PathResolver.kt @@ -1,10 +1,8 @@ package com.reactnativekb // part of https://raw.githubusercontent.com/RonRadtke/react-native-blob-util/master/android/src/main/java/com/ReactNativeBlobUtil/Utils/PathResolver.java -import android.annotation.TargetApi import android.content.Context import android.database.Cursor import android.net.Uri -import android.os.Build import android.provider.DocumentsContract import android.provider.MediaStore import android.content.ContentUris @@ -14,21 +12,19 @@ import java.io.File; import java.io.InputStream; import java.io.FileOutputStream; object PathResolver { - @TargetApi(19) fun getRealPathFromURI(context: Context?, uri: Uri?): String? { if (context == null || uri == null) { return null } - val isKitKat: Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT // DocumentProvider - if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { + if (DocumentsContract.isDocumentUri(context, uri)) { // ExternalStorageProvider if (isExternalStorageDocument(uri)) { val docId: String = DocumentsContract.getDocumentId(uri) val split: List = docId.split(":") val type = split[0] - if ("primary".equals(type, ignoreCase = true) && context != null) { + if ("primary".equals(type, ignoreCase = true)) { val dir: File? = context.getExternalFilesDir(null) return if (dir != null) dir.toString() + "/" + split[1] else "" } @@ -39,13 +35,13 @@ object PathResolver { val id: String = DocumentsContract.getDocumentId(uri) //Starting with Android O, this "id" is not necessarily a long (row number), //but might also be a "raw:/some/file/path" URL - if (id != null && id.startsWith("raw:/")) { + if (id.startsWith("raw:/")) { val rawuri: Uri = Uri.parse(id) - return rawuri.getPath() + return rawuri.path } var docId: Long? = null //Since Android 10, uri can start with msf scheme like "msf:12345" - if (id != null && id.startsWith("msf:")) { + if (id.startsWith("msf:")) { val split: List = id.split(":") val v = split[1] if (v != null) { @@ -68,40 +64,36 @@ object PathResolver { val docId: String = DocumentsContract.getDocumentId(uri) val split: List = docId.split(":") val type = split[0] - var contentUri: Uri? = null - if ("image".equals(type)) { - contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI - } else if ("video".equals(type)) { - contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI - } else if ("audio".equals(type)) { - contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + val contentUri: Uri? = when (type) { + "image" -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI + "video" -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI + "audio" -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + else -> null } val selection = "_id=?" val selectionArgs = arrayOf( split[1] ) return getDataColumn(context, contentUri, selection, selectionArgs) - } else if ("content".equals(uri.getScheme(), ignoreCase = true)) { + } else if ("content".equals(uri.scheme, ignoreCase = true)) { // Return the remote address - return if (isGooglePhotosUri(uri)) uri.getLastPathSegment() else getDataColumn(context, uri, null, null) + return if (isGooglePhotosUri(uri)) uri.lastPathSegment else getDataColumn(context, uri, null, null) } else { try { - val cr = context.getContentResolver() - if (cr != null) { - val attachment: InputStream? = cr.openInputStream(uri) - if (attachment != null) { - val filename = getContentName(context.getContentResolver(), uri) - if (filename != null) { - val file = File(context.getCacheDir(), filename) - val tmp = FileOutputStream(file) - val buffer = ByteArray(1024) - while (attachment.read(buffer) > 0) { - tmp.write(buffer) - } - tmp.close() - attachment.close() - return file.getAbsolutePath() + val cr = context.contentResolver + val attachment: InputStream? = cr.openInputStream(uri) + if (attachment != null) { + val filename = getContentName(context.contentResolver, uri) + if (filename != null) { + val file = File(context.cacheDir, filename) + val tmp = FileOutputStream(file) + val buffer = ByteArray(1024) + while (attachment.read(buffer) > 0) { + tmp.write(buffer) } + tmp.close() + attachment.close() + return file.absolutePath } } } catch (e: Exception) { @@ -109,12 +101,12 @@ object PathResolver { return null } } - } else if ("content".equals(uri.getScheme(), ignoreCase = true)) { + } else if ("content".equals(uri.scheme, ignoreCase = true)) { // Return the remote address - return if (isGooglePhotosUri(uri)) uri.getLastPathSegment() else getDataColumn(context, uri, null, null) - } else if ("file".equals(uri.getScheme(), ignoreCase = true)) { - return uri.getPath() + return if (isGooglePhotosUri(uri)) uri.lastPathSegment else getDataColumn(context, uri, null, null) + } else if ("file".equals(uri.scheme, ignoreCase = true)) { + return uri.path } return null } @@ -152,26 +144,18 @@ object PathResolver { if (context == null || uri == null) { return null } - var cursor: Cursor? = null - var result: String? = null val column = "_data" val projection = arrayOf( column ) - try { - cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, - null) - if (cursor != null && cursor.moveToFirst()) { + context.contentResolver.query(uri, projection, selection, selectionArgs, + null)?.use { cursor -> + if (cursor.moveToFirst()) { val index: Int = cursor.getColumnIndexOrThrow(column) - result = cursor.getString(index) + return cursor.getString(index) } - } catch (ex: Exception) { - ex.printStackTrace() - return null - } finally { - if (cursor != null) cursor.close() } - return result + return null } /** @@ -179,7 +163,7 @@ object PathResolver { * @return Whether the Uri authority is ExternalStorageProvider. */ fun isExternalStorageDocument(uri: Uri): Boolean { - return "com.android.externalstorage.documents".equals(uri.getAuthority()) + return "com.android.externalstorage.documents" == uri.authority } /** @@ -187,7 +171,7 @@ object PathResolver { * @return Whether the Uri authority is DownloadsProvider. */ fun isDownloadsDocument(uri: Uri): Boolean { - return "com.android.providers.downloads.documents".equals(uri.getAuthority()) + return "com.android.providers.downloads.documents" == uri.authority } /** @@ -195,7 +179,7 @@ object PathResolver { * @return Whether the Uri authority is MediaProvider. */ fun isMediaDocument(uri: Uri): Boolean { - return "com.android.providers.media.documents".equals(uri.getAuthority()) + return "com.android.providers.media.documents" == uri.authority } /** @@ -203,6 +187,6 @@ object PathResolver { * @return Whether the Uri authority is Google Photos. */ fun isGooglePhotosUri(uri: Uri): Boolean { - return "com.google.android.apps.photos.content".equals(uri.getAuthority()) + return "com.google.android.apps.photos.content" == uri.authority } } diff --git a/rnmodules/react-native-kb/android/src/main/java/com/reactnativekb/ReadFileAsString.kt b/rnmodules/react-native-kb/android/src/main/java/com/reactnativekb/ReadFileAsString.kt index cecd14f8c4fd..c4238b5f8419 100644 --- a/rnmodules/react-native-kb/android/src/main/java/com/reactnativekb/ReadFileAsString.kt +++ b/rnmodules/react-native-kb/android/src/main/java/com/reactnativekb/ReadFileAsString.kt @@ -1,38 +1,17 @@ package com.reactnativekb -import java.io.BufferedReader import java.io.File -import java.io.FileInputStream import java.io.FileNotFoundException import java.io.IOException -import java.io.InputStreamReader object ReadFileAsString { fun read(path: String): String { - var ret = "" - - try { - val inputStream = FileInputStream(File(path)) - - inputStream.use { - val inputStreamReader = InputStreamReader(it) - val bufferedReader = BufferedReader(inputStreamReader) - val stringBuilder = StringBuilder() - - var receiveString: String? - - while (bufferedReader.readLine().also { receiveString = it } != null) { - stringBuilder.append(receiveString) - } - - ret = stringBuilder.toString() - } + return try { + File(path).readText() } catch (e: FileNotFoundException) { - // ignore + "" } catch (e: IOException) { - // ignore + "" } - - return ret } } diff --git a/rnmodules/react-native-kb/cpp/react-native-kb.cpp b/rnmodules/react-native-kb/cpp/react-native-kb.cpp index 2b652cfd8a9c..1c64ebc08d54 100644 --- a/rnmodules/react-native-kb/cpp/react-native-kb.cpp +++ b/rnmodules/react-native-kb/cpp/react-native-kb.cpp @@ -1,4 +1,5 @@ #include "react-native-kb.h" +#include #include #include #include @@ -56,7 +57,8 @@ Value KBBridge::convertMPToJSI(Runtime &runtime, void *mpObj) { auto &o = *static_cast(mpObj); switch (o.type) { case msgpack::type::STR: - return jsi::String::createFromUtf8(runtime, o.as()); + return jsi::String::createFromUtf8(runtime, + reinterpret_cast(o.via.str.ptr), o.via.str.size); case msgpack::type::POSITIVE_INTEGER: return jsi::Value(o.as()); case msgpack::type::NEGATIVE_INTEGER: @@ -76,9 +78,18 @@ Value KBBridge::convertMPToJSI(Runtime &runtime, void *mpObj) { auto *p = o.via.map.ptr; auto *const pend = o.via.map.ptr + o.via.map.size; for (; p < pend; ++p) { - auto key = mpToString(p->key); auto val = convertMPToJSI(runtime, &p->val); - obj.setProperty(runtime, jsi::String::createFromUtf8(runtime, key), val); + auto &k = p->key; + if (k.type == msgpack::type::STR) { + obj.setProperty(runtime, + jsi::PropNameID::forUtf8(runtime, + reinterpret_cast(k.via.str.ptr), k.via.str.size), + val); + } else { + auto keyStr = mpToString(k); + obj.setProperty(runtime, + jsi::PropNameID::forUtf8(runtime, keyStr), val); + } } return obj; } @@ -116,6 +127,99 @@ Value KBBridge::convertMPToJSI(Runtime &runtime, void *mpObj) { } } +void KBBridge::convertJSIToMP(Runtime &runtime, const Value &value, + void *packer) { + auto &pk = *static_cast *>(packer); + if (value.isNull() || value.isUndefined()) { + pk.pack_nil(); + } else if (value.isBool()) { + pk.pack(value.getBool()); + } else if (value.isNumber()) { + double d = value.getNumber(); + // Doubles can exactly represent integers up to 2^53. Encode exact + // integers as msgpack int/uint (matching @msgpack/msgpack JS behavior) + // so Go's decoder sees integer types, not float64. + if (d == std::floor(d) && std::isfinite(d)) { + if (d >= 0) { + pk.pack(static_cast(d)); + } else { + pk.pack(static_cast(d)); + } + } else { + pk.pack(d); + } + } else if (value.isString()) { + auto str = value.getString(runtime).utf8(runtime); + pk.pack(str); + } else if (value.isObject()) { + auto obj = value.getObject(runtime); + if (obj.isArrayBuffer(runtime)) { + auto buf = obj.getArrayBuffer(runtime); + pk.pack_bin(static_cast(buf.size(runtime))); + pk.pack_bin_body(reinterpret_cast(buf.data(runtime)), + static_cast(buf.size(runtime))); + } else if (obj.isArray(runtime)) { + auto arr = obj.getArray(runtime); + auto len = arr.size(runtime); + pk.pack_array(static_cast(len)); + for (size_t i = 0; i < len; ++i) { + convertJSIToMP(runtime, arr.getValueAtIndex(runtime, i), &pk); + } + } else { + // Check for Uint8Array: has "byteLength" and "buffer" properties + // where "buffer" is an ArrayBuffer + auto byteLengthProp = obj.getProperty(runtime, "byteLength"); + if (byteLengthProp.isNumber()) { + auto bufferProp = obj.getProperty(runtime, "buffer"); + if (bufferProp.isObject()) { + auto bufferObj = bufferProp.asObject(runtime); + if (bufferObj.isArrayBuffer(runtime)) { + // This is a TypedArray (Uint8Array) — encode as BIN + auto arrayBuf = bufferObj.getArrayBuffer(runtime); + auto byteOffset = obj.getProperty(runtime, "byteOffset"); + size_t offset = byteOffset.isNumber() + ? static_cast(byteOffset.getNumber()) + : 0; + size_t length = static_cast(byteLengthProp.getNumber()); + pk.pack_bin(static_cast(length)); + pk.pack_bin_body( + reinterpret_cast(arrayBuf.data(runtime)) + offset, + static_cast(length)); + return; + } + } + } + // Regular object — encode as MAP + auto names = obj.getPropertyNames(runtime); + auto len = names.size(runtime); + pk.pack_map(static_cast(len)); + for (size_t i = 0; i < len; ++i) { + auto name = names.getValueAtIndex(runtime, i).getString(runtime); + auto nameStr = name.utf8(runtime); + pk.pack(nameStr); + convertJSIToMP(runtime, obj.getProperty(runtime, name), &pk); + } + } + } +} + +void KBBridge::packAndSend(Runtime &runtime, const Value &value) { + msgpack::sbuffer sbuf; + msgpack::packer pk(&sbuf); + convertJSIToMP(runtime, value, &pk); + + // Write framed: [length prefix][content] + msgpack::sbuffer frameBuf; + msgpack::packer framePk(&frameBuf); + framePk.pack(static_cast(sbuf.size())); + + std::vector combined(frameBuf.size() + sbuf.size()); + std::memcpy(combined.data(), frameBuf.data(), frameBuf.size()); + std::memcpy(combined.data() + frameBuf.size(), sbuf.data(), sbuf.size()); + + writeToGo_(combined.data(), combined.size()); +} + void KBBridge::install( Runtime &runtime, std::shared_ptr callInvoker, @@ -123,20 +227,16 @@ void KBBridge::install( std::function onError) { callInvoker_ = std::move(callInvoker); onError_ = std::move(onError); + writeToGo_ = std::move(writeToGo); mp_ = std::make_unique(); auto rpcOnGo = Function::createFromHostFunction( runtime, PropNameID::forAscii(runtime, "rpcOnGo"), 1, - [writeToGo = std::move(writeToGo)](Runtime &runtime, - const Value &thisValue, - const Value *arguments, - size_t count) -> Value { + [self = shared_from_this()](Runtime &runtime, const Value &thisValue, + const Value *arguments, + size_t count) -> Value { try { - auto obj = arguments[0].asObject(runtime); - auto buffer = obj.getArrayBuffer(runtime); - auto ptr = buffer.data(runtime); - auto size = buffer.size(runtime); - writeToGo(ptr, size); + self->packAndSend(runtime, arguments[0]); return Value(true); } catch (const std::exception &e) { throw std::runtime_error("Error in rpcOnGo: " + @@ -228,13 +328,29 @@ void KBBridge::onDataFromGo(uint8_t *data, int size) { } } - for (auto &result : *values) { - msgpack::object obj(result.get()); + if (values->size() == 1) { + // Single message: pass directly (no array wrapper) + msgpack::object obj((*values)[0].get()); Value value = self->convertMPToJSI(runtime, &obj); if (self->isTornDown_.load()) { return; } - self->cachedRpcOnJs_->call(runtime, std::move(value), 1); + self->cachedRpcOnJs_->call(runtime, std::move(value), + jsi::Value(1)); + } else { + // Multiple messages: batch into array, pass count + jsi::Array arr(runtime, values->size()); + for (size_t i = 0; i < values->size(); ++i) { + msgpack::object obj((*values)[i].get()); + arr.setValueAtIndex(runtime, i, + self->convertMPToJSI(runtime, &obj)); + } + if (self->isTornDown_.load()) { + return; + } + self->cachedRpcOnJs_->call( + runtime, std::move(arr), + jsi::Value(static_cast(values->size()))); } } catch (const std::exception &e) { if (self->onError_) { diff --git a/rnmodules/react-native-kb/cpp/react-native-kb.h b/rnmodules/react-native-kb/cpp/react-native-kb.h index 2197bcdd3b9a..6706b7d2353a 100644 --- a/rnmodules/react-native-kb/cpp/react-native-kb.h +++ b/rnmodules/react-native-kb/cpp/react-native-kb.h @@ -41,10 +41,15 @@ class KBBridge : public std::enable_shared_from_this { std::unique_ptr cachedUint8ArrayCtor_; std::unique_ptr cachedRpcOnJs_; facebook::jsi::Runtime *cachedRuntime_ = nullptr; + std::function writeToGo_; void resetCaches(facebook::jsi::Runtime &runtime); facebook::jsi::Value convertMPToJSI(facebook::jsi::Runtime &runtime, void *mpObj); + void convertJSIToMP(facebook::jsi::Runtime &runtime, + const facebook::jsi::Value &value, void *packer); + void packAndSend(facebook::jsi::Runtime &runtime, + const facebook::jsi::Value &value); }; } // namespace kb diff --git a/rnmodules/react-native-kb/ios/Kb.mm b/rnmodules/react-native-kb/ios/Kb.mm index d24371aff485..94ace5701cef 100644 --- a/rnmodules/react-native-kb/ios/Kb.mm +++ b/rnmodules/react-native-kb/ios/Kb.mm @@ -1,7 +1,5 @@ #import "Kb.h" #import "Keybasego.h" -#import -#import #import #import #import @@ -87,13 +85,13 @@ + (void)swizzleUITextViewPaste { Class cls = [UITextView class]; SEL originalPaste = @selector(paste:); - SEL swizzledPaste = @selector(kb_paste:); + SEL swizzledPaste = NSSelectorFromString(@"kb_paste:"); Method originalPasteMethod = class_getInstanceMethod(cls, originalPaste); Method swizzledPasteMethod = class_getInstanceMethod(cls, swizzledPaste); method_exchangeImplementations(originalPasteMethod, swizzledPasteMethod); SEL originalCanPerform = @selector(canPerformAction:withSender:); - SEL swizzledCanPerform = @selector(kb_canPerformAction:withSender:); + SEL swizzledCanPerform = NSSelectorFromString(@"kb_canPerformAction:withSender:"); Method originalCanPerformMethod = class_getInstanceMethod(cls, originalCanPerform); Method swizzledCanPerformMethod = class_getInstanceMethod(cls, swizzledCanPerform); method_exchangeImplementations(originalCanPerformMethod, swizzledCanPerformMethod); @@ -313,10 +311,10 @@ - (NSDictionary *)getConstants { RCT_EXPORT_METHOD(getDefaultCountryCode : (RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock)reject) { - CTTelephonyNetworkInfo *network_Info = [CTTelephonyNetworkInfo new]; - // TODO this will stop working at some point - CTCarrier *carrier = network_Info.subscriberCellularProvider; - resolve(carrier.isoCountryCode); + // CTCarrier was removed in iOS 16.4 with no replacement. + // Use the locale's region code instead — good enough for phone number formatting. + NSString *countryCode = [[NSLocale currentLocale] countryCode]; + resolve(countryCode ?: @""); } RCT_EXPORT_METHOD(logSend:(NSString *)status feedback:(NSString *)feedback sendLogs:(BOOL)sendLogs sendMaxBytes:(BOOL)sendMaxBytes traceDir:(NSString *)traceDir cpuProfileDir:(NSString *)cpuProfileDir resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { @@ -501,10 +499,6 @@ + (void)setInitialNotification:(NSDictionary *)notification { } + (void)emitPushNotification:(NSDictionary *)notification { - NSString *type = notification[@"type"] ?: @"unknown"; - NSString *convID = notification[@"convID"] ?: notification[@"c"] ?: @"unknown"; - NSNumber *userInteraction = notification[@"userInteraction"]; - if (kbSharedInstance) { [kbSharedInstance sendEventWithName:@"onPushNotification" body:notification]; NSLog(@"Kb.emitPushNotification: sent event 'onPushNotification' to JS"); @@ -533,7 +527,7 @@ - (NSNumber *)androidSetSecureFlagSetting:(BOOL)s {return @-1;} - (NSNumber *)androidShare:(NSString *)text mimeType:(NSString *)mimeType {return @-1;} - (NSNumber *)androidShareText:(NSString *)text mimeType:(NSString *)mimeType {return @-1;} - (NSString *)androidGetRegistrationToken {return @"";} -- (void)androidAddCompleteDownload:(/*JS::NativeKb::SpecAndroidAddCompleteDownloadO &*/id)o resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {} +- (void)androidAddCompleteDownload:(JS::NativeKb::SpecAndroidAddCompleteDownloadO &)o resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {} - (void)androidAppColorSchemeChanged:(NSString *)mode {} - (void)androidCheckPushPermissions:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {} - (void)androidGetRegistrationToken:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {} diff --git a/rnmodules/react-native-kb/react-native-kb.podspec b/rnmodules/react-native-kb/react-native-kb.podspec index fd69e6c875ef..5091221340d4 100644 --- a/rnmodules/react-native-kb/react-native-kb.podspec +++ b/rnmodules/react-native-kb/react-native-kb.podspec @@ -26,7 +26,7 @@ Pod::Spec.new do |s| # See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79. if respond_to?(:install_modules_dependencies, true) s.pod_target_xcconfig = { - "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\" $(PODS_ROOT)/../../node_modules/msgpack-cxx-6.1.0/include $(PODS_ROOT)/../keybasego.xcframework/ios-arm64/Keybasego.framework/Headers \"$(PODS_CONFIGURATION_BUILD_DIR)/KBCommon\"", + "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\" $(PODS_ROOT)/../../node_modules/msgpack-cxx-7.0.0/include $(PODS_ROOT)/../keybasego.xcframework/ios-arm64/Keybasego.framework/Headers \"$(PODS_CONFIGURATION_BUILD_DIR)/KBCommon\"", "OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -DMSGPACK_NO_BOOST=1", "CLANG_CXX_LANGUAGE_STANDARD" => "c++17" } @@ -38,7 +38,7 @@ Pod::Spec.new do |s| if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1" s.pod_target_xcconfig = { - "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\" $(PODS_ROOT)/../../node_modules/msgpack-cxx-6.1.0/include $(PODS_ROOT)/../keybasego.xcframework/ios-arm64/Keybasego.framework/Headers \"$(PODS_CONFIGURATION_BUILD_DIR)/KBCommon\"", + "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\" $(PODS_ROOT)/../../node_modules/msgpack-cxx-7.0.0/include $(PODS_ROOT)/../keybasego.xcframework/ios-arm64/Keybasego.framework/Headers \"$(PODS_CONFIGURATION_BUILD_DIR)/KBCommon\"", "OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -DMSGPACK_NO_BOOST=1", "CLANG_CXX_LANGUAGE_STANDARD" => "c++17" } @@ -49,7 +49,7 @@ Pod::Spec.new do |s| s.dependency "ReactCommon/turbomodule/core" else s.pod_target_xcconfig = { - "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\" $(PODS_ROOT)/../../node_modules/msgpack-cxx-6.1.0/include $(PODS_ROOT)/../keybasego.xcframework/ios-arm64/Keybasego.framework/Headers \"$(PODS_CONFIGURATION_BUILD_DIR)/KBCommon\"", + "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\" $(PODS_ROOT)/../../node_modules/msgpack-cxx-7.0.0/include $(PODS_ROOT)/../keybasego.xcframework/ios-arm64/Keybasego.framework/Headers \"$(PODS_CONFIGURATION_BUILD_DIR)/KBCommon\"", "OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -DMSGPACK_NO_BOOST=1", "CLANG_CXX_LANGUAGE_STANDARD" => "c++17" } diff --git a/shared/android/app/src/main/java/io/keybase/ossifrage/ChatBroadcastReceiver.kt b/shared/android/app/src/main/java/io/keybase/ossifrage/ChatBroadcastReceiver.kt index e7ce43df86cc..afb2f3a7dd03 100644 --- a/shared/android/app/src/main/java/io/keybase/ossifrage/ChatBroadcastReceiver.kt +++ b/shared/android/app/src/main/java/io/keybase/ossifrage/ChatBroadcastReceiver.kt @@ -5,9 +5,7 @@ import android.app.RemoteInput import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import android.os.Build import android.os.Bundle -import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import io.keybase.ossifrage.MainActivity.Companion.setupKBRuntime @@ -15,16 +13,14 @@ import io.keybase.ossifrage.modules.NativeLogger import keybase.Keybase class ChatBroadcastReceiver : BroadcastReceiver() { - @RequiresApi(api = Build.VERSION_CODES.KITKAT_WATCH) private fun getMessageText(intent: Intent): String? { val remoteInput = RemoteInput.getResultsFromIntent(intent) return remoteInput?.getCharSequence(KEY_TEXT_REPLY)?.toString() } - @RequiresApi(api = Build.VERSION_CODES.KITKAT_WATCH) override fun onReceive(context: Context, intent: Intent) { setupKBRuntime(context, false) - val convData = ConvData(intent) + val convData = ConvData.fromIntent(intent) val openConv = intent.getParcelableExtra("openConvPendingIntent") val repliedNotification = NotificationCompat.Builder(context, KeybasePushNotificationListenerService.CHAT_CHANNEL_ID) .setContentIntent(openConv) @@ -53,30 +49,15 @@ class ChatBroadcastReceiver : BroadcastReceiver() { } companion object { - @JvmField - var KEY_TEXT_REPLY = "key_text_reply" + const val KEY_TEXT_REPLY = "key_text_reply" } } -internal class ConvData { - @JvmField - var convID: String? - var tlfName: String? - var lastMsgId: Long - - constructor(convId: String?, tlfName: String?, lastMsgId: Long) { - convID = convId - this.tlfName = tlfName - this.lastMsgId = lastMsgId - } - - constructor(intent: Intent) { - val data = intent.getBundleExtra("ConvData") - convID = data!!.getString("convID") - tlfName = data.getString("tlfName") - lastMsgId = data.getLong("lastMsgId") - } - +internal data class ConvData( + @JvmField val convID: String?, + val tlfName: String?, + val lastMsgId: Long +) { fun intoIntent(context: Context?): Intent { val data = Bundle() data.putString("convID", convID) @@ -86,4 +67,15 @@ internal class ConvData { intent.putExtra("ConvData", data) return intent } + + companion object { + fun fromIntent(intent: Intent): ConvData { + val data = intent.getBundleExtra("ConvData")!! + return ConvData( + convID = data.getString("convID"), + tlfName = data.getString("tlfName"), + lastMsgId = data.getLong("lastMsgId") + ) + } + } } diff --git a/shared/android/app/src/main/java/io/keybase/ossifrage/CustomBitmapMemoryCacheParamsSupplier.kt b/shared/android/app/src/main/java/io/keybase/ossifrage/CustomBitmapMemoryCacheParamsSupplier.kt index a7197ca9c4da..734810b2a6c9 100644 --- a/shared/android/app/src/main/java/io/keybase/ossifrage/CustomBitmapMemoryCacheParamsSupplier.kt +++ b/shared/android/app/src/main/java/io/keybase/ossifrage/CustomBitmapMemoryCacheParamsSupplier.kt @@ -2,7 +2,6 @@ package io.keybase.ossifrage import android.app.ActivityManager import android.content.Context -import android.os.Build import com.facebook.common.internal.Supplier import com.facebook.common.util.ByteConstants import com.facebook.imagepipeline.cache.MemoryCacheParams @@ -27,12 +26,10 @@ class CustomBitmapMemoryCacheParamsSupplier(context: Context) : Supplier 4 * ByteConstants.MB + maxMemory < 64 * ByteConstants.MB -> 6 * ByteConstants.MB + else -> maxMemory / CACHE_DIVISION } } diff --git a/shared/android/app/src/main/java/io/keybase/ossifrage/KBInstallReferrerListener.kt b/shared/android/app/src/main/java/io/keybase/ossifrage/KBInstallReferrerListener.kt index 6e3644f6f608..030b077e0644 100644 --- a/shared/android/app/src/main/java/io/keybase/ossifrage/KBInstallReferrerListener.kt +++ b/shared/android/app/src/main/java/io/keybase/ossifrage/KBInstallReferrerListener.kt @@ -36,17 +36,17 @@ class KBInstallReferrerListener internal constructor(_context: Context) : Native override fun onInstallReferrerSetupFinished(responseCode: Int) { Log.e("KBIR", "KBInstallReferrerListener#onInstallReferrerSetupFinished: got code $responseCode") - executor.execute(Runnable { + executor.execute { when (responseCode) { InstallReferrerClient.InstallReferrerResponse.OK -> { // Connection established handleReferrerResponseOK() - return@Runnable + return@execute } InstallReferrerClient.InstallReferrerResponse.SERVICE_DISCONNECTED -> { reconnect() - return@Runnable + return@execute } InstallReferrerClient.InstallReferrerResponse.FEATURE_NOT_SUPPORTED, InstallReferrerClient.InstallReferrerResponse.SERVICE_UNAVAILABLE, InstallReferrerClient.InstallReferrerResponse.DEVELOPER_ERROR -> // other issues, can't do much here.... @@ -54,7 +54,7 @@ class KBInstallReferrerListener internal constructor(_context: Context) : Native else -> callback!!.callbackWithString("") } - }) + } } private fun handleReferrerResponseOK() { diff --git a/shared/android/app/src/main/java/io/keybase/ossifrage/KBPushNotifier.kt b/shared/android/app/src/main/java/io/keybase/ossifrage/KBPushNotifier.kt index 2d071a144bfa..1ff93094f718 100644 --- a/shared/android/app/src/main/java/io/keybase/ossifrage/KBPushNotifier.kt +++ b/shared/android/app/src/main/java/io/keybase/ossifrage/KBPushNotifier.kt @@ -11,35 +11,26 @@ import android.graphics.PorterDuff import android.graphics.PorterDuffXfermode import android.graphics.Rect import android.net.Uri -import android.os.Build import android.os.Bundle import android.os.Handler import android.os.Looper -import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.app.Person import androidx.core.app.RemoteInput import androidx.core.graphics.drawable.IconCompat -import io.keybase.ossifrage.MainActivity import keybase.ChatNotification import keybase.PushNotifier import java.io.BufferedInputStream import java.io.IOException -import java.io.InputStream import java.net.HttpURLConnection import java.net.URL -import android.util.Log class KBPushNotifier internal constructor(private val context: Context, private val bundle: Bundle) : PushNotifier { private var convMsgCache: SmallMsgRingBuffer? = null private fun buildStyle(person: Person): NotificationCompat.MessagingStyle { val style = NotificationCompat.MessagingStyle(person) - if (convMsgCache != null) { - for (msg in convMsgCache!!.summary()) { - style.addMessage(msg) - } - } + convMsgCache?.summary()?.forEach { style.addMessage(it) } return style } @@ -79,7 +70,6 @@ class KBPushNotifier internal constructor(private val context: Context, private } } - @RequiresApi(api = Build.VERSION_CODES.KITKAT_WATCH) private fun newReplyAction(context: Context, convData: ConvData, openConv: PendingIntent): NotificationCompat.Action { val replyLabel = "Reply" val remoteInput = RemoteInput.Builder(ChatBroadcastReceiver.KEY_TEXT_REPLY) @@ -131,33 +121,26 @@ class KBPushNotifier internal constructor(private val context: Context, private notificationDefaults = notificationDefaults or NotificationCompat.DEFAULT_SOUND } else { val soundResource = filenameResourceName(chatNotification.soundName) - val soundUriStr = "android.resource://" + context.packageName + "/raw/" + soundResource + val soundUriStr = "android.resource://${context.packageName}/raw/$soundResource" val soundUri = Uri.parse(soundUriStr) builder.setSound(soundUri) } builder.setDefaults(notificationDefaults) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) { - builder.addAction(newReplyAction(context, convData, pending_intent)) - } + builder.addAction(newReplyAction(context, convData, pending_intent)) val msg = chatNotification.message val from = msg.from val personBuilder = Person.Builder() .setName(from?.keybaseUsername ?: "") .setBot(from?.isBot ?: false) val avatarUri = chatNotification.message.from?.keybaseAvatar - if (avatarUri != null && avatarUri.isNotEmpty()) { - val icon = getKeybaseAvatar(avatarUri) - if (icon != null) { - personBuilder.setIcon(icon) - } - } + avatarUri?.takeIf { it.isNotEmpty() }?.let { getKeybaseAvatar(it) }?.let { personBuilder.setIcon(it) } val fromPerson = personBuilder.build() - if (convMsgCache != null) { + convMsgCache?.let { cache -> var msgText = if (chatNotification.isPlaintext) chatNotification.message.plaintext else "" if (msgText.isEmpty()) { msgText = chatNotification.message.serverMessage } - convMsgCache!!.add(NotificationCompat.MessagingStyle.Message(msgText, msg.at, fromPerson)) + cache.add(NotificationCompat.MessagingStyle.Message(msgText, msg.at, fromPerson)) } val style = buildStyle(fromPerson) style.setConversationTitle(chatNotification.conversationName ?: "") diff --git a/shared/android/app/src/main/java/io/keybase/ossifrage/KBReactPackage.kt b/shared/android/app/src/main/java/io/keybase/ossifrage/KBReactPackage.kt index 4e3dd8d8c697..d47069274059 100644 --- a/shared/android/app/src/main/java/io/keybase/ossifrage/KBReactPackage.kt +++ b/shared/android/app/src/main/java/io/keybase/ossifrage/KBReactPackage.kt @@ -7,11 +7,10 @@ import com.facebook.react.uimanager.ViewManager open class KBReactPackage : ReactPackage { override fun createNativeModules(reactApplicationContext: ReactApplicationContext): List { - // modules.add(); - return ArrayList() + return emptyList() } override fun createViewManagers(reactApplicationContext: ReactApplicationContext): List> { - return mutableListOf() + return emptyList() } } diff --git a/shared/android/app/src/main/java/io/keybase/ossifrage/KeyStore.kt b/shared/android/app/src/main/java/io/keybase/ossifrage/KeyStore.kt index 66c815069010..f13e4b8ce83e 100644 --- a/shared/android/app/src/main/java/io/keybase/ossifrage/KeyStore.kt +++ b/shared/android/app/src/main/java/io/keybase/ossifrage/KeyStore.kt @@ -3,7 +3,6 @@ package io.keybase.ossifrage import android.annotation.SuppressLint import android.content.Context import android.content.SharedPreferences -import android.os.Build import android.security.keystore.KeyPermanentlyInvalidatedException import android.util.Base64 import io.keybase.ossifrage.keystore.KeyStoreHelper @@ -60,14 +59,14 @@ class KeyStore(private val context: Context, private val prefs: SharedPreference NativeLogger.info("KeyStore: getting users with stored secrets for $serviceName") return try { val keyIterator: Iterator = prefs.all.keys.iterator() - val userNames = ArrayList() + val userNames = mutableListOf() while (keyIterator.hasNext()) { val key = keyIterator.next() if (key.indexOf(sharedPrefKeyPrefix(serviceName)) == 0) { userNames.add(key.substring(sharedPrefKeyPrefix(serviceName).length)) } } - NativeLogger.info("KeyStore: got " + userNames.size + " users with stored secrets for " + serviceName) + NativeLogger.info("KeyStore: got ${userNames.size} users with stored secrets for $serviceName") val packer = MessagePack.newDefaultBufferPacker() packer.packArrayHeader(userNames.size) for (s in userNames) { @@ -91,16 +90,16 @@ class KeyStore(private val context: Context, private val prefs: SharedPreference val entry = ks.getEntry(keyStoreAlias(serviceName), null) ?: throw KeyStoreException("No RSA keys in the keystore") if (entry !is KeyStore.PrivateKeyEntry) { - throw KeyStoreException("Entry is not a PrivateKeyEntry. It is: " + entry.javaClass) + throw KeyStoreException("Entry is not a PrivateKeyEntry. It is: ${entry.javaClass}") } try { val secret = unwrapSecret(entry, wrappedSecret).encoded - NativeLogger.info("KeyStore: retrieved " + secret.size + "-byte secret for " + id) + NativeLogger.info("KeyStore: retrieved ${secret.size}-byte secret for $id") secret } catch (e: InvalidKeyException) { // Invalid key, this can happen when a user changes their lock screen from something to nothing // or enrolls a new finger. See https://developer.android.com/reference/android/security/keystore/KeyPermanentlyInvalidatedException.html - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && e is KeyPermanentlyInvalidatedException) { + if (e is KeyPermanentlyInvalidatedException) { NativeLogger.info("KeyStore: key no longer valid; deleting entry", e) ks.deleteEntry(keyStoreAlias(serviceName)) } @@ -150,7 +149,7 @@ class KeyStore(private val context: Context, private val prefs: SharedPreference @Throws(Exception::class) override fun storeSecret(serviceName: String, key: String, bytes: ByteArray) { val id = "$serviceName:$key" - NativeLogger.info("KeyStore: storing " + bytes.size + "-byte secret for " + id) + NativeLogger.info("KeyStore: storing ${bytes.size}-byte secret for $id") try { val entry = ks.getEntry(keyStoreAlias(serviceName), null) ?: throw KeyStoreException("No RSA keys in the keystore") @@ -160,7 +159,7 @@ class KeyStore(private val context: Context, private val prefs: SharedPreference NativeLogger.error("KeyStore: error storing secret for $id", e) throw e } - NativeLogger.info("KeyStore: stored " + bytes.size + "-byte secret for " + id) + NativeLogger.info("KeyStore: stored ${bytes.size}-byte secret for $id") } companion object { diff --git a/shared/android/app/src/main/java/io/keybase/ossifrage/KeybasePushNotificationListenerService.kt b/shared/android/app/src/main/java/io/keybase/ossifrage/KeybasePushNotificationListenerService.kt index ec0a2d5e9398..da3d2698d3b4 100644 --- a/shared/android/app/src/main/java/io/keybase/ossifrage/KeybasePushNotificationListenerService.kt +++ b/shared/android/app/src/main/java/io/keybase/ossifrage/KeybasePushNotificationListenerService.kt @@ -7,7 +7,6 @@ import android.os.Build import android.os.Bundle import android.os.Handler import android.os.Looper -import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.app.Person @@ -16,11 +15,9 @@ import com.google.firebase.messaging.RemoteMessage import io.keybase.ossifrage.MainActivity.Companion.setupKBRuntime import io.keybase.ossifrage.modules.NativeLogger import keybase.Keybase -import keybase.ChatNotification import com.reactnativekb.KbModule import org.json.JSONArray import org.json.JSONObject -import android.util.Log class KeybasePushNotificationListenerService : FirebaseMessagingService() { // This keeps a small ring buffer cache of the last 5 messages per conversation the user @@ -244,14 +241,10 @@ class KeybasePushNotificationListenerService : FirebaseMessagingService() { } companion object { - @JvmField - var CHAT_CHANNEL_ID = "kb_chat_channel" - @JvmField - var FOLLOW_CHANNEL_ID = "kb_follow_channel" - @JvmField - var DEVICE_CHANNEL_ID = "kb_device_channel" - @JvmField - var GENERAL_CHANNEL_ID = "kb_rest_channel" + const val CHAT_CHANNEL_ID = "kb_chat_channel" + const val FOLLOW_CHANNEL_ID = "kb_follow_channel" + const val DEVICE_CHANNEL_ID = "kb_device_channel" + const val GENERAL_CHANNEL_ID = "kb_rest_channel" fun createNotificationChannel(context: Context) { // Create the NotificationChannel, but only on API 26+ because // the NotificationChannel class is new and not in the support library @@ -323,7 +316,7 @@ class SmallMsgRingBuffer { } } -internal class NotificationData @RequiresApi(api = Build.VERSION_CODES.HONEYCOMB_MR1) constructor(type: String, bundle: Bundle) { +internal class NotificationData(type: String, bundle: Bundle) { val displayPlaintext: Boolean val membersType: Int var convID: String? = null diff --git a/shared/android/app/src/main/java/io/keybase/ossifrage/MainActivity.kt b/shared/android/app/src/main/java/io/keybase/ossifrage/MainActivity.kt index 00b112ea4e2d..f292022869e1 100644 --- a/shared/android/app/src/main/java/io/keybase/ossifrage/MainActivity.kt +++ b/shared/android/app/src/main/java/io/keybase/ossifrage/MainActivity.kt @@ -79,11 +79,10 @@ class MainActivity : ReactActivity() { super.onCreate(null) Handler(Looper.getMainLooper()).postDelayed({ try { - var gc = GuiConfig.getInstance(filesDir) - if (gc != null) { - setBackgroundColor(gc.getDarkMode()) - } + val gc = GuiConfig.getInstance(filesDir) + gc?.let { setBackgroundColor(it.getDarkMode()) } } catch (e: Exception) { + NativeLogger.warn("Error reading GuiConfig in onCreate", e) } }, 300) KeybasePushNotificationListenerService.createNotificationChannel(this) @@ -258,6 +257,7 @@ class MainActivity : ReactActivity() { // Avoid getParcelableArrayListExtra() here: some senders incorrectly use ACTION_SEND_MULTIPLE // but provide a single Uri in EXTRA_STREAM, which would cause a ClassCast log/warning. + @Suppress("DEPRECATION") when (val streamExtra = intent.extras?.get(Intent.EXTRA_STREAM)) { is Uri -> uris.add(streamExtra) is ArrayList<*> -> streamExtra.filterIsInstance().forEach { uris.add(it) } @@ -425,11 +425,10 @@ class MainActivity : ReactActivity() { override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) try { - var gc = GuiConfig.getInstance(filesDir) - if (gc != null) { - setBackgroundColor(gc.getDarkMode()) - } + val gc = GuiConfig.getInstance(filesDir) + gc?.let { setBackgroundColor(it.getDarkMode()) } } catch (e: Exception) { + NativeLogger.warn("Error reading GuiConfig in onConfigurationChanged", e) } if (newConfig.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_NO) { isUsingHardwareKeyboard = true @@ -439,13 +438,12 @@ class MainActivity : ReactActivity() { } fun setBackgroundColor(pref: DarkModePreference) { - val bgColor: Int - bgColor = if (pref == DarkModePreference.System) { - if (colorSchemeForCurrentConfiguration() == "light") R.color.white else R.color.black - } else if (pref == DarkModePreference.AlwaysDark) { - R.color.black - } else { - R.color.white + val bgColor = when (pref) { + DarkModePreference.System -> { + if (colorSchemeForCurrentConfiguration() == "light") R.color.white else R.color.black + } + DarkModePreference.AlwaysDark -> R.color.black + DarkModePreference.AlwaysLight -> R.color.white } val mainWindow = this.window val handler = Handler(Looper.getMainLooper()) diff --git a/shared/android/app/src/main/java/io/keybase/ossifrage/MainApplication.kt b/shared/android/app/src/main/java/io/keybase/ossifrage/MainApplication.kt index 5fc27ec8065b..1ebf6bac74d0 100644 --- a/shared/android/app/src/main/java/io/keybase/ossifrage/MainApplication.kt +++ b/shared/android/app/src/main/java/io/keybase/ossifrage/MainApplication.kt @@ -1,5 +1,3 @@ -@file:Suppress("DEPRECATION") - package io.keybase.ossifrage import android.app.Application @@ -8,7 +6,6 @@ import android.content.res.Configuration import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner -import androidx.multidex.MultiDex import androidx.work.PeriodicWorkRequest import androidx.work.WorkManager import androidx.work.WorkRequest @@ -30,7 +27,6 @@ import io.keybase.ossifrage.modules.BackgroundSyncWorker import io.keybase.ossifrage.modules.NativeLogger import keybase.Keybase import java.util.concurrent.TimeUnit -import expo.modules.ApplicationLifecycleDispatcher import expo.modules.ReactNativeHostWrapper internal class AppLifecycleListener(private val context: Context?) : @@ -40,6 +36,7 @@ internal class AppLifecycleListener(private val context: Context?) : try { Glide.get(context!!).clearDiskCache() } catch (e: Exception) { + NativeLogger.warn("AppLifecycleListener: error clearing Glide disk cache", e) } }.start() } @@ -95,11 +92,6 @@ class MainApplication : Application(), ReactApplication { ) } - override fun attachBaseContext(base: Context) { - super.attachBaseContext(base) - MultiDex.install(this) - } - override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) onConfigurationChanged(this, newConfig) diff --git a/shared/android/app/src/main/java/io/keybase/ossifrage/keystore/KeyStoreHelper.kt b/shared/android/app/src/main/java/io/keybase/ossifrage/keystore/KeyStoreHelper.kt index 5cd423eeacde..5be3d797d87b 100644 --- a/shared/android/app/src/main/java/io/keybase/ossifrage/keystore/KeyStoreHelper.kt +++ b/shared/android/app/src/main/java/io/keybase/ossifrage/keystore/KeyStoreHelper.kt @@ -1,8 +1,6 @@ package io.keybase.ossifrage.keystore -import android.annotation.TargetApi import android.content.Context -import android.os.Build import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties import java.math.BigInteger @@ -11,41 +9,22 @@ import java.security.KeyPairGenerator import java.security.KeyStoreException import java.security.NoSuchAlgorithmException import java.security.NoSuchProviderException -import java.security.spec.AlgorithmParameterSpec import java.util.Calendar import javax.security.auth.x500.X500Principal object KeyStoreHelper { - @TargetApi(Build.VERSION_CODES.KITKAT) @Throws(KeyStoreException::class, NoSuchProviderException::class, NoSuchAlgorithmException::class, InvalidAlgorithmParameterException::class) fun generateRSAKeyPair(ctx: Context?, keyAlias: String) { val kpg = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore") - val spec: AlgorithmParameterSpec - val endTime = Calendar.getInstance() - endTime.add(Calendar.YEAR, 10) - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - spec = KeyGenParameterSpec.Builder( - keyAlias, - KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT) - .setBlockModes(KeyProperties.BLOCK_MODE_ECB) - .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1) - .setCertificateSerialNumber(BigInteger.ONE) - .setCertificateSubject(X500Principal("CN=$keyAlias")) - .setKeySize(2048) - .build() - } else { - @Suppress("DEPRECATION") - spec = android.security.KeyPairGeneratorSpec.Builder(ctx!!) - .setAlias(keyAlias) - .setEncryptionRequired() - .setSerialNumber(BigInteger.ONE) - .setSubject(X500Principal("CN=$keyAlias")) - .setStartDate(Calendar.getInstance().time) - .setEndDate(endTime.time) - .setKeySize(2048) - .build() - } + val spec = KeyGenParameterSpec.Builder( + keyAlias, + KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT) + .setBlockModes(KeyProperties.BLOCK_MODE_ECB) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1) + .setCertificateSerialNumber(BigInteger.ONE) + .setCertificateSubject(X500Principal("CN=$keyAlias")) + .setKeySize(2048) + .build() kpg.initialize(spec) kpg.generateKeyPair() } diff --git a/shared/android/app/src/main/java/io/keybase/ossifrage/modules/NativeLogger.kt b/shared/android/app/src/main/java/io/keybase/ossifrage/modules/NativeLogger.kt index c185214df84c..cfd9c3eb532d 100644 --- a/shared/android/app/src/main/java/io/keybase/ossifrage/modules/NativeLogger.kt +++ b/shared/android/app/src/main/java/io/keybase/ossifrage/modules/NativeLogger.kt @@ -18,7 +18,7 @@ class NativeLogger(reactContext: ReactApplicationContext?) : ReactContextBaseJav private fun formatLine(tagPrefix: String, toLog: String): String { // Copies the Style JS outputs in native/logger.native.tsx - return tagPrefix + NAME + ": [" + System.currentTimeMillis() + ",\"" + toLog + "\"]" + return "${tagPrefix}${NAME}: [${System.currentTimeMillis()},\"$toLog\"]" } fun error(log: String) { diff --git a/shared/android/app/src/main/java/io/keybase/ossifrage/modules/StorybookConstants.kt b/shared/android/app/src/main/java/io/keybase/ossifrage/modules/StorybookConstants.kt index e24f193135ab..b888175bdafd 100644 --- a/shared/android/app/src/main/java/io/keybase/ossifrage/modules/StorybookConstants.kt +++ b/shared/android/app/src/main/java/io/keybase/ossifrage/modules/StorybookConstants.kt @@ -6,8 +6,8 @@ import io.keybase.ossifrage.BuildConfig class StorybookConstants(reactContext: ReactApplicationContext?) : ReactContextBaseJavaModule(reactContext) { override fun getConstants(): Map? { - val isStoryBook = BuildConfig.BUILD_TYPE === "storyBook" - val constants: MutableMap = HashMap() + val isStoryBook = BuildConfig.BUILD_TYPE == "storyBook" + val constants: MutableMap = mutableMapOf() constants["isStorybook"] = isStoryBook return constants } diff --git a/shared/android/app/src/main/java/io/keybase/ossifrage/util/DeviceLockType.kt b/shared/android/app/src/main/java/io/keybase/ossifrage/util/DeviceLockType.kt index 649ce9328e2a..11a60a3bf0c9 100644 --- a/shared/android/app/src/main/java/io/keybase/ossifrage/util/DeviceLockType.kt +++ b/shared/android/app/src/main/java/io/keybase/ossifrage/util/DeviceLockType.kt @@ -73,25 +73,26 @@ object DeviceLockType { fun getCurrent(contentResolver: ContentResolver?): Int { val mode = Settings.Secure.getLong(contentResolver, PASSWORD_TYPE_KEY, DevicePolicyManager.PASSWORD_QUALITY_SOMETHING.toLong()) - return if (mode == DevicePolicyManager.PASSWORD_QUALITY_SOMETHING.toLong()) { - @Suppress("DEPRECATION") - if (Settings.Secure.getInt(contentResolver, Settings.Secure.LOCK_PATTERN_ENABLED, 0) == 1) { - PATTERN - } else NONE_OR_SLIDER - } else if (mode == DevicePolicyManager.PASSWORD_QUALITY_BIOMETRIC_WEAK.toLong()) { - val dataDirPath = Environment.getDataDirectory().absolutePath - if (nonEmptyFileExists("$dataDirPath/system/gesture.key")) { - FACE_WITH_PATTERN - } else if (nonEmptyFileExists("$dataDirPath/system/password.key")) { - FACE_WITH_PIN - } else FACE_WITH_SOMETHING_ELSE - } else if (mode == DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC.toLong()) { - PASSWORD_ALPHANUMERIC - } else if (mode == DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC.toLong()) { - PASSWORD_ALPHABETIC - } else if (mode == DevicePolicyManager.PASSWORD_QUALITY_NUMERIC.toLong()) { - PIN - } else SOMETHING_ELSE + return when (mode) { + DevicePolicyManager.PASSWORD_QUALITY_SOMETHING.toLong() -> { + @Suppress("DEPRECATION") + if (Settings.Secure.getInt(contentResolver, Settings.Secure.LOCK_PATTERN_ENABLED, 0) == 1) { + PATTERN + } else NONE_OR_SLIDER + } + DevicePolicyManager.PASSWORD_QUALITY_BIOMETRIC_WEAK.toLong() -> { + val dataDirPath = Environment.getDataDirectory().absolutePath + if (nonEmptyFileExists("$dataDirPath/system/gesture.key")) { + FACE_WITH_PATTERN + } else if (nonEmptyFileExists("$dataDirPath/system/password.key")) { + FACE_WITH_PIN + } else FACE_WITH_SOMETHING_ELSE + } + DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC.toLong() -> PASSWORD_ALPHANUMERIC + DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC.toLong() -> PASSWORD_ALPHABETIC + DevicePolicyManager.PASSWORD_QUALITY_NUMERIC.toLong() -> PIN + else -> SOMETHING_ELSE + } } private fun nonEmptyFileExists(filename: String): Boolean { diff --git a/shared/desktop/yarn-helper/index.tsx b/shared/desktop/yarn-helper/index.tsx index 181b987ef3ef..0a18d84f846d 100644 --- a/shared/desktop/yarn-helper/index.tsx +++ b/shared/desktop/yarn-helper/index.tsx @@ -125,8 +125,8 @@ const decorateInfo = (info: Command) => { const getMsgPack = () => { if (process.platform === 'darwin') { - const ver = '6.1.0' - const shasum = '09b6b71cdfb4b176e5bb12b02b4ffc290ec10b41' + const ver = '7.0.0' + const shasum = '37bbdbf69ef44392c7af215b9cb419891a9e1c9c' const file = `msgpack-cxx-${ver}.tar.gz` const url = `https://github.com/msgpack/msgpack-c/releases/download/cpp-${ver}/${file}` const prefix = path.resolve(__dirname, '..', '..', 'node_modules') diff --git a/shared/engine/index.platform.native.tsx b/shared/engine/index.platform.native.tsx index 929d4979eb4e..4cd92255bcac 100644 --- a/shared/engine/index.platform.native.tsx +++ b/shared/engine/index.platform.native.tsx @@ -1,5 +1,4 @@ import {TransportShared, sharedCreateClient, rpcLog} from './transport-shared' -import {encode} from '@msgpack/msgpack' import type {IncomingRPCCallbackType, ConnectDisconnectCB} from './index.platform' import logger from '@/logger' import {engineReset, getNativeEmitter, notifyJSReady} from 'react-native-kb' @@ -33,17 +32,11 @@ class NativeTransport extends TransportShared { // A custom send override to write to the react native bridge send(msg: unknown) { - const packed = encode(msg) - const len = encode(packed.length) - const buf = new Uint8Array(len.length + packed.length) - buf.set(len, 0) - buf.set(packed, len.length) - // Pass data over to the native side to be handled, with JSI! try { if (!global.rpcOnGo) { logger.error('>>>> rpcOnGo send before rpcOnGo global?') } - global.rpcOnGo?.(buf.buffer) + global.rpcOnGo?.(msg) } catch (e) { logger.error('>>>> rpcOnGo JS thrown!', e) } @@ -60,9 +53,16 @@ function createClient( new NativeTransport(incomingRPCCallback, connectCallback, disconnectCallback) ) - global.rpcOnJs = (objs: unknown) => { + global.rpcOnJs = (objs: unknown, count: number) => { try { - client.transport._dispatch(objs) + if (count > 1) { + const arr = objs as Array + for (const obj of arr) { + client.transport._dispatch(obj) + } + } else { + client.transport._dispatch(objs) + } } catch (e) { logger.error('>>>> rpcOnJs JS thrown!', e) } diff --git a/shared/globals.d.ts b/shared/globals.d.ts index 4aa82e8af833..f8ca8884020a 100644 --- a/shared/globals.d.ts +++ b/shared/globals.d.ts @@ -34,8 +34,8 @@ declare global { var __VERSION__: string var __FILE_SUFFIX__: string var __PROFILE__: boolean - var rpcOnGo: undefined | ((b: unknown) => void) - var rpcOnJs: undefined | ((b: unknown) => void) + var rpcOnGo: undefined | ((msg: unknown) => void) + var rpcOnJs: undefined | ((objs: unknown, count: number) => void) // RN var __turboModuleProxy: unknown } diff --git a/shared/ios/Keybase/AppDelegate.swift b/shared/ios/Keybase/AppDelegate.swift index ac5158070149..e102c7307b19 100644 --- a/shared/ios/Keybase/AppDelegate.swift +++ b/shared/ios/Keybase/AppDelegate.swift @@ -1,3 +1,4 @@ +import BackgroundTasks import Expo import React import ReactAppDependencyProvider @@ -7,6 +8,9 @@ import UserNotifications import AVFoundation import ExpoModulesCore import Keybasego +import os + +private let log = Logger(subsystem: "com.keybase.app", category: "delegate") class KeyboardWindow: UIWindow { override func pressesBegan(_ presses: Set, with event: UIPressesEvent?) { @@ -32,7 +36,7 @@ class KeyboardWindow: UIWindow { } } -@UIApplicationMain +@main public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UIDropInteractionDelegate { var window: UIWindow? @@ -67,7 +71,7 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID } NotificationCenter.default.addObserver(forName: UIApplication.didReceiveMemoryWarningNotification, object: nil, queue: .main) { [weak self] notification in - NSLog("Memory warning received - deferring GC during React Native initialization") + log.info("Memory warning received - deferring GC during React Native initialization") // see if this helps avoid this crash DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { guard let self = self, self.reactNativeFactory != nil else { return } @@ -86,7 +90,8 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID bindReactNativeFactory(factory) #if os(iOS) || os(tvOS) - window = KeyboardWindow(frame: UIScreen.main.bounds) + let screenBounds = (UIApplication.shared.connectedScenes.first as? UIWindowScene)?.screen.bounds ?? UIScreen.main.bounds + window = KeyboardWindow(frame: screenBounds) factory.startReactNative( withModuleName: "Keybase", in: window, @@ -138,18 +143,13 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID } if self.startupLogFileHandle == nil { - do { - if !FileManager.default.fileExists(atPath: logFilePath) { - FileManager.default.createFile(atPath: logFilePath, contents: nil, attributes: nil) - } + if !FileManager.default.fileExists(atPath: logFilePath) { + FileManager.default.createFile(atPath: logFilePath, contents: nil, attributes: nil) + } - if let fileHandle = FileHandle(forWritingAtPath: logFilePath) { - fileHandle.seekToEndOfFile() - self.startupLogFileHandle = fileHandle - } - } catch { - NSLog("Error opening startup timing log file: \(error)") - return + if let fileHandle = try? FileHandle(forWritingTo: URL(fileURLWithPath: logFilePath)) { + try? fileHandle.seekToEnd() + self.startupLogFileHandle = fileHandle } } @@ -166,26 +166,22 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) let dateString = dateFormatter.string(from: now) let timestamp = String(format: "%@.%06dZ", dateString, microseconds) - - let fileName = (file as NSString).lastPathComponent - let logMessage = String(format: "%@ ▶ [DEBU keybase %@:%d] Delegate startup: %@\n", timestamp, fileName, line, message) + + let fileName = URL(fileURLWithPath: file).lastPathComponent + let logMessage = String(format: "%@ \u{25B6} [DEBU keybase %@:%d] Delegate startup: %@\n", timestamp, fileName, line, message) guard let logData = logMessage.data(using: .utf8) else { return } - do { - fileHandle.write(logData) - fileHandle.synchronizeFile() - } catch { - NSLog("Error writing startup timing log: \(error)") - } + try? fileHandle.write(contentsOf: logData) + try? fileHandle.synchronize() } private func closeStartupLogFile() { if let fileHandle = self.startupLogFileHandle { - fileHandle.synchronizeFile() - fileHandle.closeFile() + try? fileHandle.synchronize() + try? fileHandle.close() self.startupLogFileHandle = nil } } @@ -208,18 +204,18 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID self.writeStartupTimingLog("Before Go init") // Initialize Go synchronously - happens during splash screen - NSLog("Starting KeybaseInit (synchronous)...") + log.info("Starting KeybaseInit (synchronous)...") var err: NSError? let shareIntentDonator = ShareIntentDonatorImpl() Keybasego.KeybaseInit(self.fsPaths["homedir"], self.fsPaths["sharedHome"], self.fsPaths["logFile"], "prod", securityAccessGroupOverride, nil, nil, systemVer, isIPad, nil, isIOS, shareIntentDonator, &err) - if let err { NSLog("KeybaseInit FAILED: \(err)") } - + if let err { log.error("KeybaseInit FAILED: \(err.localizedDescription, privacy: .public)") } + self.writeStartupTimingLog("After Go init") } func notifyAppState(_ application: UIApplication) { let state = application.applicationState - NSLog("notifyAppState: notifying service with new appState: \(state.rawValue)") + log.info("notifyAppState: notifying service with new appState: \(state.rawValue)") switch state { case .active: Keybasego.KeybaseSetAppStateForeground() case .background: Keybasego.KeybaseSetAppStateBackground() @@ -240,11 +236,12 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID rootView.backgroundColor = .systemBackground // Snapshot resizing workaround for iPad - var dim = UIScreen.main.bounds.width - if UIScreen.main.bounds.height > dim { - dim = UIScreen.main.bounds.height + let screenBounds = self.window?.windowScene?.screen.bounds ?? UIScreen.main.bounds + var dim = screenBounds.width + if screenBounds.height > dim { + dim = screenBounds.height } - let square = CGRect(origin: UIScreen.main.bounds.origin, size: CGSize(width: dim, height: dim)) + let square = CGRect(origin: screenBounds.origin, size: CGSize(width: dim, height: dim)) self.resignImageView = UIImageView(frame: square) self.resignImageView?.contentMode = .center self.resignImageView?.alpha = 0 @@ -252,7 +249,10 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID self.resignImageView?.image = UIImage(named: "LaunchImage") self.window?.addSubview(self.resignImageView!) - UIApplication.shared.setMinimumBackgroundFetchInterval(UIApplication.backgroundFetchIntervalMinimum) + BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.keybase.app.refresh", using: nil) { task in + self.handleAppRefresh(task: task as! BGAppRefreshTask) + } + scheduleAppRefresh() } func addDrop(_ rootView: UIView) { @@ -282,12 +282,28 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID self.iph?.startProcessing() } - public override func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { - NSLog("Background fetch started...") + func scheduleAppRefresh() { + let request = BGAppRefreshTaskRequest(identifier: "com.keybase.app.refresh") + request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) + do { + try BGTaskScheduler.shared.submit(request) + } catch { + log.error("Could not schedule app refresh: \(error.localizedDescription, privacy: .public)") + } + } + + func handleAppRefresh(task: BGAppRefreshTask) { + scheduleAppRefresh() + + task.expirationHandler = { + log.warning("Background refresh task expired") + } + DispatchQueue.global(qos: .default).async { + log.info("Background fetch started...") Keybasego.KeybaseBackgroundSync() - completionHandler(.newData) - NSLog("Background fetch completed...") + task.setTaskCompleted(success: true) + log.info("Background fetch completed...") } } @@ -315,9 +331,9 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID var err: NSError? Keybasego.KeybaseHandleBackgroundNotification(convID, body, "", sender, membersType, displayPlaintext, messageID, pushID, badgeCount, unixTime, soundName, pusher, false, &err) - if let err { NSLog("Failed to handle in engine: \(err)") } + if let err { log.error("Failed to handle in engine: \(err.localizedDescription, privacy: .public)") } completionHandler(.newData) - NSLog("Remote notification handle finished...") + log.info("Remote notification handle finished...") } } else { var notificationDict = Dictionary(uniqueKeysWithValues: notification.map { (String(describing: $0.key), $0.value) }) @@ -332,9 +348,6 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID var notificationDict = Dictionary(uniqueKeysWithValues: userInfo.map { (String(describing: $0.key), $0.value) }) notificationDict["userInteraction"] = true - let type = notificationDict["type"] as? String ?? "unknown" - let convID = notificationDict["convID"] as? String ?? notificationDict["c"] as? String ?? "unknown" - // Store the notification so it can be processed when app becomes active // This ensures navigation works even if React Native isn't ready yet KbSetInitialNotification(notificationDict) @@ -358,40 +371,39 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID } func hideCover() { - NSLog("hideCover: cancelling outstanding animations...") + log.info("hideCover: cancelling outstanding animations...") self.resignImageView?.layer.removeAllAnimations() self.resignImageView?.alpha = 0 } public override func applicationWillResignActive(_ application: UIApplication) { - NSLog("applicationWillResignActive: cancelling outstanding animations...") + log.info("applicationWillResignActive: cancelling outstanding animations...") self.resignImageView?.layer.removeAllAnimations() self.resignImageView?.superview?.bringSubviewToFront(self.resignImageView!) - NSLog("applicationWillResignActive: rendering keyz screen...") + log.info("applicationWillResignActive: rendering keyz screen...") UIView.animate(withDuration: 0.3, delay: 0.1, options: .beginFromCurrentState) { self.resignImageView?.alpha = 1 } completion: { finished in - NSLog("applicationWillResignActive: rendered keyz screen. Finished: \(finished)") + log.info("applicationWillResignActive: rendered keyz screen. Finished: \(finished)") } Keybasego.KeybaseSetAppStateInactive() } public override func applicationDidEnterBackground(_ application: UIApplication) { PerfFPSMonitor.appDidEnterBackground() - application.ignoreSnapshotOnNextApplicationLaunch() - NSLog("applicationDidEnterBackground: cancelling outstanding animations...") + log.info("applicationDidEnterBackground: cancelling outstanding animations...") self.resignImageView?.layer.removeAllAnimations() - NSLog("applicationDidEnterBackground: setting keyz screen alpha to 1.") + log.info("applicationDidEnterBackground: setting keyz screen alpha to 1.") self.resignImageView?.alpha = 1 - NSLog("applicationDidEnterBackground: notifying go.") + log.info("applicationDidEnterBackground: notifying go.") let requestTime = Keybasego.KeybaseAppDidEnterBackground() - NSLog("applicationDidEnterBackground: after notifying go.") + log.info("applicationDidEnterBackground: after notifying go.") if requestTime && (self.shutdownTask == UIBackgroundTaskIdentifier.invalid) { let app = UIApplication.shared self.shutdownTask = app.beginBackgroundTask { - NSLog("applicationDidEnterBackground: shutdown task run.") + log.info("applicationDidEnterBackground: shutdown task run.") Keybasego.KeybaseAppWillExit(PushNotifier()) let task = self.shutdownTask if task != .invalid { @@ -412,17 +424,15 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID } public override func applicationDidBecomeActive(_ application: UIApplication) { - NSLog("applicationDidBecomeActive: hiding keyz screen.") + log.info("applicationDidBecomeActive: hiding keyz screen.") hideCover() - NSLog("applicationDidBecomeActive: notifying service.") + log.info("applicationDidBecomeActive: notifying service.") notifyAppState(application) // Check if there's a stored notification with userInteraction that needs to be processed // This handles the case where app was backgrounded and notification was clicked // but React Native wasn't ready yet if let storedNotification = KbGetAndClearInitialNotification() { - let type = storedNotification["type"] as? String ?? "unknown" - let convID = storedNotification["convID"] as? String ?? storedNotification["c"] as? String ?? "unknown" let userInteraction = storedNotification["userInteraction"] as? Bool ?? false if userInteraction { @@ -430,22 +440,22 @@ public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate, UID if alreadyReEmitted { KbSetInitialNotification(storedNotification) } else { - NSLog("applicationDidBecomeActive: stored notification has userInteraction=true, emitting") + log.info("applicationDidBecomeActive: stored notification has userInteraction=true, emitting") KbEmitPushNotification(storedNotification) - var copy = Dictionary(uniqueKeysWithValues: storedNotification.map { (String(describing: $0.key), $0.value) }) + var copy = storedNotification copy["reEmittedInBecomeActive"] = true - KbSetInitialNotification(copy as NSDictionary as! [AnyHashable : Any]) + KbSetInitialNotification(copy) } } else { - NSLog("applicationDidBecomeActive: stored notification has userInteraction=false, skipping") + log.info("applicationDidBecomeActive: stored notification has userInteraction=false, skipping") } } else { - NSLog("applicationDidBecomeActive: no stored notification found") + log.info("applicationDidBecomeActive: no stored notification found") } } public override func applicationWillEnterForeground(_ application: UIApplication) { - NSLog("applicationWillEnterForeground: hiding keyz screen.") + log.info("applicationWillEnterForeground: hiding keyz screen.") hideCover() } diff --git a/shared/ios/Keybase/Fs.swift b/shared/ios/Keybase/Fs.swift index 10dc2b486a99..1b1975df5cc7 100644 --- a/shared/ios/Keybase/Fs.swift +++ b/shared/ios/Keybase/Fs.swift @@ -1,30 +1,35 @@ import Foundation +import os + +private let log = Logger(subsystem: "com.keybase.app", category: "fs") @objc class FsHelper: NSObject { + private static let cacheKeybaseURL = URL(fileURLWithPath: NSHomeDirectory()).appendingPathComponent("Library/Caches/Keybase") + private static let appSupportKeybaseURL = URL(fileURLWithPath: NSHomeDirectory()).appendingPathComponent("Library/Application Support/Keybase") + @objc func setupFs(_ skipLogFile: Bool, setupSharedHome shouldSetupSharedHome: Bool) -> [String: String] { let setupFsStartTime = CFAbsoluteTimeGetCurrent() - NSLog("setupFs: starting") + log.info("setupFs: starting") var home = NSHomeDirectory() let sharedURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.keybase") - var sharedHome = sharedURL?.relativePath ?? "" + var sharedHome = sharedURL?.path ?? "" home = setupAppHome(home: home, sharedHome: sharedHome) if shouldSetupSharedHome { sharedHome = setupSharedHome(home: home, sharedHome: sharedHome) } - let appKeybasePath = Self.getAppKeybasePath() + let appKeybaseURL = URL(fileURLWithPath: Self.getAppKeybasePath()) // Put logs in a subdir that is entirely background readable - let oldLogPath = ("~/Library/Caches/Keybase" as NSString).expandingTildeInPath - let logPath = (oldLogPath as NSString).appendingPathComponent("logs") - let serviceLogFile = skipLogFile ? "" : (logPath as NSString).appendingPathComponent("ios.log") + let logURL = Self.cacheKeybaseURL.appendingPathComponent("logs") + let serviceLogFile = skipLogFile ? "" : logURL.appendingPathComponent("ios.log").path if !skipLogFile { // cleanup old log files let fm = FileManager.default ["ios.log", "ios.log.ek"].forEach { - try? fm.removeItem(atPath: (oldLogPath as NSString).appendingPathComponent($0)) + try? fm.removeItem(at: Self.cacheKeybaseURL.appendingPathComponent($0)) } } // Create LevelDB and log directories with a slightly lower data protection @@ -44,13 +49,13 @@ import Foundation "synced_tlf_config", "logs" ].forEach { - createBackgroundReadableDirectory(path: (appKeybasePath as NSString).appendingPathComponent($0), setAllFiles: true) + createBackgroundReadableDirectory(path: appKeybaseURL.appendingPathComponent($0).path, setAllFiles: true) } // Mark avatars, which are in the caches dir - createBackgroundReadableDirectory(path: (oldLogPath as NSString).appendingPathComponent("avatars"), setAllFiles: true) + createBackgroundReadableDirectory(path: Self.cacheKeybaseURL.appendingPathComponent("avatars").path, setAllFiles: true) let setupFsElapsed = CFAbsoluteTimeGetCurrent() - setupFsStartTime - NSLog("setupFs: completed in %.3f seconds", setupFsElapsed) + log.info("setupFs: completed in \(setupFsElapsed, format: .fixed(precision: 3)) seconds") return [ "home": home, @@ -60,12 +65,14 @@ import Foundation } private func addSkipBackupAttribute(to path: String) -> Bool { - let url = Foundation.URL(fileURLWithPath: path) + var url = URL(fileURLWithPath: path) do { - try (url as NSURL).setResourceValue(true, forKey: URLResourceKey.isExcludedFromBackupKey) + var resourceValues = URLResourceValues() + resourceValues.isExcludedFromBackup = true + try url.setResourceValues(resourceValues) return true } catch { - NSLog("Error excluding \(url.lastPathComponent) from backup \(error)") + log.error("Error excluding \(url.lastPathComponent, privacy: .public) from backup \(error.localizedDescription, privacy: .public)") return false } } @@ -78,82 +85,82 @@ import Foundation // files are still stored on the disk encrypted (note for the chat database, // it means we are encrypting it twice), and are inaccessible otherwise. let noProt = [FileAttributeKey.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication] - NSLog("creating background readable directory: path: \(path) setAllFiles: \(setAllFiles)") + log.info("creating background readable directory: path: \(path, privacy: .public) setAllFiles: \(setAllFiles)") _ = try? fm.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: noProt) do { try fm.setAttributes(noProt, ofItemAtPath: path) } catch { - NSLog("Error setting file attributes on path: \(path) error: \(error)") + log.error("Error setting file attributes on path: \(path, privacy: .public) error: \(error.localizedDescription, privacy: .public)") } guard setAllFiles else { - NSLog("setAllFiles is false, so returning now") + log.info("setAllFiles is false, so returning now") return } - NSLog("setAllFiles is true charging forward") + log.info("setAllFiles is true charging forward") // Recursively set attributes on all subdirectories and files + let pathURL = URL(fileURLWithPath: path) var fileCount = 0 if let enumerator = fm.enumerator(atPath: path) { for case let file as String in enumerator { - let filePath = (path as NSString).appendingPathComponent(file) + let filePath = pathURL.appendingPathComponent(file).path do { try fm.setAttributes(noProt, ofItemAtPath: filePath) fileCount += 1 } catch { - NSLog("Error setting file attributes on: \(filePath) error: \(error)") + log.error("Error setting file attributes on: \(filePath, privacy: .public) error: \(error.localizedDescription, privacy: .public)") } } let dirElapsed = CFAbsoluteTimeGetCurrent() - dirStartTime - NSLog("createBackgroundReadableDirectory completed for: \(path), processed \(fileCount) files, total: %.3f seconds", dirElapsed) + log.info("createBackgroundReadableDirectory completed for: \(path, privacy: .public), processed \(fileCount) files, total: \(dirElapsed, format: .fixed(precision: 3)) seconds") } else { - NSLog("Error creating enumerator for path: \(path)") + log.error("Error creating enumerator for path: \(path, privacy: .public)") } } private func maybeMigrateDirectory(source: String, dest: String) -> Bool { let fm = FileManager.default + let sourceURL = URL(fileURLWithPath: source) + let destURL = URL(fileURLWithPath: dest) do { // Always do this move in case it doesn't work on previous attempts. let sourceContents = try fm.contentsOfDirectory(atPath: source) for file in sourceContents { - let path = (source as NSString).appendingPathComponent(file) - let destPath = (dest as NSString).appendingPathComponent(file) + let filePath = sourceURL.appendingPathComponent(file).path + let destPath = destURL.appendingPathComponent(file).path var isDir: ObjCBool = false - if fm.fileExists(atPath: path, isDirectory: &isDir), isDir.boolValue { - NSLog("skipping directory: \(file)") + if fm.fileExists(atPath: filePath, isDirectory: &isDir), isDir.boolValue { + log.info("skipping directory: \(file, privacy: .public)") continue } do { - try fm.moveItem(atPath: path, toPath: destPath) + try fm.moveItem(atPath: filePath, toPath: destPath) } catch let error as NSError { if error.code == NSFileWriteFileExistsError { continue } - NSLog("Error moving file: \(file) error: \(error)") + log.error("Error moving file: \(file, privacy: .public) error: \(error.localizedDescription, privacy: .public)") return false } } return true } catch { - NSLog("Error listing app contents directory: \(error)") + log.error("Error listing app contents directory: \(error.localizedDescription, privacy: .public)") return false } } @objc static func getAppKeybasePath() -> String { - return ("~/Library/Application Support/Keybase" as NSString).expandingTildeInPath + return appSupportKeybaseURL.path } @objc static func getEraseableKVPath() -> String { - return (getAppKeybasePath() as NSString).appendingPathComponent("eraseablekvstore/device-eks") + return appSupportKeybaseURL.appendingPathComponent("eraseablekvstore/device-eks").path } private func setupAppHome(home: String, sharedHome: String) -> String { - let tempUrl = FileManager.default.temporaryDirectory - // workaround a problem where iOS dyld3 loader crashes if accessing .closure files - // with complete data protection on - let dyldDir = (tempUrl.path as NSString).appendingPathComponent("com.apple.dyld") + let dyldDir = FileManager.default.temporaryDirectory.appendingPathComponent("com.apple.dyld").path let appKeybasePath = Self.getAppKeybasePath() let appEraseableKVPath = Self.getEraseableKVPath() @@ -168,8 +175,8 @@ import Foundation private func setupSharedHome(home: String, sharedHome: String) -> String { let appKeybasePath = Self.getAppKeybasePath() let appEraseableKVPath = Self.getEraseableKVPath() - let sharedKeybasePath = (sharedHome as NSString).appendingPathComponent("Library/Application Support/Keybase") - let sharedEraseableKVPath = (sharedKeybasePath as NSString).appendingPathComponent("eraseablekvstore/device-eks") + let sharedKeybasePath = URL(fileURLWithPath: sharedHome).appendingPathComponent("Library/Application Support/Keybase").path + let sharedEraseableKVPath = URL(fileURLWithPath: sharedKeybasePath).appendingPathComponent("eraseablekvstore/device-eks").path createBackgroundReadableDirectory(path: sharedKeybasePath, setAllFiles: true) createBackgroundReadableDirectory(path: sharedEraseableKVPath, setAllFiles: true) diff --git a/shared/ios/Keybase/Info.plist b/shared/ios/Keybase/Info.plist index fc0bc7c9f8f2..1285b1876ca9 100644 --- a/shared/ios/Keybase/Info.plist +++ b/shared/ios/Keybase/Info.plist @@ -87,6 +87,10 @@ SourceCodePro-Semibold.ttf kb.ttf + BGTaskSchedulerPermittedIdentifiers + + com.keybase.app.refresh + UIBackgroundModes fetch diff --git a/shared/ios/Keybase/PerfFPSMonitor.swift b/shared/ios/Keybase/PerfFPSMonitor.swift index 278eed6905ee..a0d97bd6378d 100644 --- a/shared/ios/Keybase/PerfFPSMonitor.swift +++ b/shared/ios/Keybase/PerfFPSMonitor.swift @@ -1,6 +1,9 @@ import Foundation import QuartzCore import UIKit +import os + +private let log = Logger(subsystem: "com.keybase.app", category: "perf") /// Lightweight FPS monitor using CADisplayLink. /// Activated by the `-PERF_FPS_MONITOR` launch argument. @@ -35,7 +38,7 @@ import UIKit // Use .common so it fires during scroll tracking displayLink?.add(to: .main, forMode: .common) - NSLog("PerfFPSMonitor: started") + log.info("PerfFPSMonitor: started") } func stop() { @@ -48,7 +51,7 @@ import UIKit samples.append(frameCount) } writeResults() - NSLog("PerfFPSMonitor: stopped, wrote %d samples to %@", samples.count, PerfFPSMonitor.outputPath) + log.info("PerfFPSMonitor: stopped, wrote \(self.samples.count) samples to \(PerfFPSMonitor.outputPath, privacy: .public)") } @objc private func tick(_ link: CADisplayLink) { diff --git a/shared/ios/Keybase/Pusher.swift b/shared/ios/Keybase/Pusher.swift index a32c59669693..5dd98596f484 100644 --- a/shared/ios/Keybase/Pusher.swift +++ b/shared/ios/Keybase/Pusher.swift @@ -1,6 +1,9 @@ import Foundation import UserNotifications import Keybasego +import os + +private let log = Logger(subsystem: "com.keybase.app", category: "push") class PushNotifier: NSObject, Keybasego.KeybasePushNotifierProtocol { func localNotification(_ ident: String?, msg: String?, badgeCount: Int, soundName: String?, convID: String?, typ: String?) { @@ -14,15 +17,15 @@ class PushNotifier: NSObject, Keybasego.KeybasePushNotifierProtocol { let request = UNNotificationRequest(identifier: ident ?? UUID().uuidString, content: content, trigger: nil) UNUserNotificationCenter.current().add(request) { error in if let error = error { - NSLog("local notification failed: %@", error.localizedDescription) + log.error("local notification failed: \(error.localizedDescription, privacy: .public)") } } } - + func display(_ n: KeybaseChatNotification?) { guard let notification = n else { return } guard let message = notification.message else { return } - + let ident = "\(notification.convID):\(message.id_)" let msg: String if notification.isPlaintext && !message.plaintext.isEmpty { diff --git a/shared/ios/Keybase/ShareIntentDonatorImpl.swift b/shared/ios/Keybase/ShareIntentDonatorImpl.swift index c5bde51cd30a..98af4182ba52 100644 --- a/shared/ios/Keybase/ShareIntentDonatorImpl.swift +++ b/shared/ios/Keybase/ShareIntentDonatorImpl.swift @@ -9,6 +9,9 @@ import Foundation import Intents import Keybasego import UIKit +import os + +private let log = Logger(subsystem: "com.keybase.app", category: "share") private struct ShareConversation: Decodable { let convID: String @@ -29,29 +32,29 @@ private struct ShareConversation: Decodable { class ShareIntentDonatorImpl: NSObject, Keybasego.KeybaseShareIntentDonatorProtocol { func deleteAllDonations() { INInteraction.deleteAll { _ in } - NSLog("ShareIntentDonator: deleteAllDonations completed") + log.info("ShareIntentDonator: deleteAllDonations completed") } func deleteDonation(_ conversationID: String?) { guard let id = conversationID, !id.isEmpty else { return } INInteraction.delete(with: id, completion: nil) - NSLog("ShareIntentDonator: deleteDonation completed for %@", id) + log.info("ShareIntentDonator: deleteDonation completed for \(id, privacy: .public)") } func donateShareConversations(_ conversationsJSON: String?) { guard let json = conversationsJSON, let data = json.data(using: .utf8) else { - NSLog("ShareIntentDonator: donateShareConversations: nil or invalid JSON") + log.info("ShareIntentDonator: donateShareConversations: nil or invalid JSON") return } guard let conversations = try? JSONDecoder().decode([ShareConversation].self, from: data) else { - NSLog("ShareIntentDonator: donateShareConversations: JSON decode failed") + log.info("ShareIntentDonator: donateShareConversations: JSON decode failed") return } guard !conversations.isEmpty else { - NSLog("ShareIntentDonator: donateShareConversations: empty conversations array") + log.info("ShareIntentDonator: donateShareConversations: empty conversations array") return } - NSLog("ShareIntentDonator: donateShareConversations: donating %d conversations", conversations.count) + log.info("ShareIntentDonator: donateShareConversations: donating \(conversations.count) conversations") donateConversations(conversations) } @@ -163,7 +166,7 @@ class ShareIntentDonatorImpl: NSObject, Keybasego.KeybaseShareIntentDonatorProto let interaction = INInteraction(intent: intent, response: nil) interaction.donate { error in if let error = error { - NSLog("ShareIntentDonator: donateIntent failed for %@: %@", intent.conversationIdentifier ?? "?", error.localizedDescription) + log.error("ShareIntentDonator: donateIntent failed for \(intent.conversationIdentifier ?? "?", privacy: .public): \(error.localizedDescription, privacy: .public)") } } } diff --git a/shared/ios/KeybaseShare/ShareViewController.swift b/shared/ios/KeybaseShare/ShareViewController.swift index 163e8a5e7668..e303025814bf 100644 --- a/shared/ios/KeybaseShare/ShareViewController.swift +++ b/shared/ios/KeybaseShare/ShareViewController.swift @@ -26,19 +26,15 @@ public class ShareViewController: UIViewController { func openApp() { let path = selectedConvID.map { "keybase://incoming-share/\($0)" } ?? "keybase://incoming-share" guard let url = URL(string: path) else { return } + let sel = #selector(UIApplication.open(_:options:completionHandler:)) var responder: UIResponder? = self while let r = responder { - if r.responds(to: #selector(UIApplication.openURL(_:))) { - do { - let sel = #selector(UIApplication.open(_:options:completionHandler:)) - if r.responds(to: sel) { - let imp = r.method(for: sel) - typealias Func = @convention(c) (AnyObject, Selector, URL, [UIApplication.OpenExternalURLOptionsKey: Any], ((Bool) -> Void)?) -> Void - let f = unsafeBitCast(imp, to: Func.self) - f(r, sel, url, [:], nil) - return - } - } + if r.responds(to: sel) { + let imp = r.method(for: sel) + typealias Func = @convention(c) (AnyObject, Selector, URL, [UIApplication.OpenExternalURLOptionsKey: Any], ((Bool) -> Void)?) -> Void + let f = unsafeBitCast(imp, to: Func.self) + f(r, sel, url, [:], nil) + return } responder = r.next } @@ -80,9 +76,8 @@ public class ShareViewController: UIViewController { ($0 as? NSExtensionItem)?.attachments } ?? [] - weak var weakSelf = self - iph = ItemProviderHelper(forShare: true, withItems: itemArrs) { - guard let self = weakSelf else { return } + iph = ItemProviderHelper(forShare: true, withItems: itemArrs) { [weak self] in + guard let self else { return } self.completeRequestAlreadyInMainThread() } showProgressView() diff --git a/shared/ios/Podfile.lock b/shared/ios/Podfile.lock index a0996a92717c..ba1142c1091a 100644 --- a/shared/ios/Podfile.lock +++ b/shared/ios/Podfile.lock @@ -3384,7 +3384,7 @@ SPEC CHECKSUMS: React-logger: a913317214a26565cd4c045347edf1bcacb80a3f React-Mapbuffer: 017336879e2e0fb7537bbc08c24f34e2384c9260 React-microtasksnativemodule: 63ee6730cec233feab9cdcc0c100dc28a12e4165 - react-native-kb: 078843e8c52d210aff0c50cbb4c4abe888c28ee2 + react-native-kb: 47269c30b862f82a1556f88cc6f00dbee91a9a98 react-native-keyboard-controller: a9e423beaa20d00a4b9664b0fa37f1cdf3a59975 react-native-netinfo: 9fad4eedfec9840a10e73ac4591ea1158523309b react-native-safe-area-context: 0a3b034bb63a5b684dd2f5fffd3c90ef6ed41ee8 diff --git a/shared/package.json b/shared/package.json index e6db987c71c5..74159c4dbd82 100644 --- a/shared/package.json +++ b/shared/package.json @@ -60,6 +60,7 @@ "android-unsigned": "react-native run-android --mode 'releaseUnsigned' --appId io.keybase.ossifrage.unsigned", "android-install-downgrade-unsigned": "adb install -r -d android/app/build/outputs/apk/releaseUnsigned/app-releaseUnsigned.apk", "android-logs": "adb logcat | grep $(adb shell pidof io.keybase.ossifrage)", + "android-logs-dump": "adb logcat -d --pid=$(adb shell pidof io.keybase.ossifrage)", "android-log-clear": "adb logcat -b all -c", "clean-and-install": "rm -rf node_modules ; yarn pod-clean ; yarn modules ; yarn pod-install", "coverage": "rm -rf coverage-ts; npx typescript-coverage-report",