From 2a32810182ddf0f2eac93db87ea7ee8ab3e3dfae Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 10 Sep 2022 21:49:51 +0100 Subject: [PATCH 01/11] nix: android/log.h patch --- flake.nix | 5 +- scripts/nix/direct-sqlcipher-android.patch | 324 +++++++++++++++++++++ 2 files changed, 328 insertions(+), 1 deletion(-) create mode 100644 scripts/nix/direct-sqlcipher-android.patch diff --git a/flake.nix b/flake.nix index 08a1e18dd2..1b1d494f57 100644 --- a/flake.nix +++ b/flake.nix @@ -25,7 +25,10 @@ }; sha256map = import ./scripts/nix/sha256map.nix; modules = [{ - packages.direct-sqlcipher.patches = [ ./scripts/nix/direct-sqlcipher-2.3.27.patch ]; + packages.direct-sqlcipher.patches = [ + "./scripts/nix/direct-sqlcipher-2.3.27.patch", + "./scripts/nix/direct-sqlcipher-android.patch" + ]; packages.entropy.patches = [ ./scripts/nix/entropy.patch ]; } ({ pkgs,lib, ... }: lib.mkIf (pkgs.stdenv.hostPlatform.isAndroid) { diff --git a/scripts/nix/direct-sqlcipher-android.patch b/scripts/nix/direct-sqlcipher-android.patch new file mode 100644 index 0000000000..225172e4f2 --- /dev/null +++ b/scripts/nix/direct-sqlcipher-android.patch @@ -0,0 +1,324 @@ +diff --git a/cbits/android/log.h b/cbits/android/log.h +new file mode 100644 +index 0000000..c8e715e +--- /dev/null ++++ b/cbits/android/log.h +@@ -0,0 +1,318 @@ ++/* ++ * Copyright (C) 2009 The Android Open Source Project ++ * ++ * Licensed under the Apache License, Version 2.0 (the "License"); ++ * you may not use this file except in compliance with the License. ++ * You may obtain a copy of the License at ++ * ++ * http://www.apache.org/licenses/LICENSE-2.0 ++ * ++ * Unless required by applicable law or agreed to in writing, software ++ * distributed under the License is distributed on an "AS IS" BASIS, ++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++ * See the License for the specific language governing permissions and ++ * limitations under the License. ++ */ ++#pragma once ++/** ++ * @addtogroup Logging ++ * @{ ++ */ ++/** ++ * \file ++ * ++ * Support routines to send messages to the Android log buffer, ++ * which can later be accessed through the `logcat` utility. ++ * ++ * Each log message must have ++ * - a priority ++ * - a log tag ++ * - some text ++ * ++ * The tag normally corresponds to the component that emits the log message, ++ * and should be reasonably small. ++ * ++ * Log message text may be truncated to less than an implementation-specific ++ * limit (1023 bytes). ++ * ++ * Note that a newline character ("\n") will be appended automatically to your ++ * log message, if not already there. It is not possible to send several ++ * messages and have them appear on a single line in logcat. ++ * ++ * Please use logging in moderation: ++ * ++ * - Sending log messages eats CPU and slow down your application and the ++ * system. ++ * ++ * - The circular log buffer is pretty small, so sending many messages ++ * will hide other important log messages. ++ * ++ * - In release builds, only send log messages to account for exceptional ++ * conditions. ++ */ ++#include ++#include ++#include ++#include ++#if !defined(__BIONIC__) && !defined(__INTRODUCED_IN) ++#define __INTRODUCED_IN(x) ++#endif ++#ifdef __cplusplus ++extern "C" { ++#endif ++/** ++ * Android log priority values, in increasing order of priority. ++ */ ++typedef enum android_LogPriority { ++ /** For internal use only. */ ++ ANDROID_LOG_UNKNOWN = 0, ++ /** The default priority, for internal use only. */ ++ ANDROID_LOG_DEFAULT, /* only for SetMinPriority() */ ++ /** Verbose logging. Should typically be disabled for a release apk. */ ++ ANDROID_LOG_VERBOSE, ++ /** Debug logging. Should typically be disabled for a release apk. */ ++ ANDROID_LOG_DEBUG, ++ /** Informational logging. Should typically be disabled for a release apk. */ ++ ANDROID_LOG_INFO, ++ /** Warning logging. For use with recoverable failures. */ ++ ANDROID_LOG_WARN, ++ /** Error logging. For use with unrecoverable failures. */ ++ ANDROID_LOG_ERROR, ++ /** Fatal logging. For use when aborting. */ ++ ANDROID_LOG_FATAL, ++ /** For internal use only. */ ++ ANDROID_LOG_SILENT, /* only for SetMinPriority(); must be last */ ++} android_LogPriority; ++/** ++ * Writes the constant string `text` to the log, with priority `prio` and tag ++ * `tag`. ++ */ ++int __android_log_write(int prio, const char* tag, const char* text); ++/** ++ * Writes a formatted string to the log, with priority `prio` and tag `tag`. ++ * The details of formatting are the same as for ++ * [printf(3)](http://man7.org/linux/man-pages/man3/printf.3.html). ++ */ ++int __android_log_print(int prio, const char* tag, const char* fmt, ...) ++ __attribute__((__format__(printf, 3, 4))); ++/** ++ * Equivalent to `__android_log_print`, but taking a `va_list`. ++ * (If `__android_log_print` is like `printf`, this is like `vprintf`.) ++ */ ++int __android_log_vprint(int prio, const char* tag, const char* fmt, va_list ap) ++ __attribute__((__format__(printf, 3, 0))); ++/** ++ * Writes an assertion failure to the log (as `ANDROID_LOG_FATAL`) and to ++ * stderr, before calling ++ * [abort(3)](http://man7.org/linux/man-pages/man3/abort.3.html). ++ * ++ * If `fmt` is non-null, `cond` is unused. If `fmt` is null, the string ++ * `Assertion failed: %s` is used with `cond` as the string argument. ++ * If both `fmt` and `cond` are null, a default string is provided. ++ * ++ * Most callers should use ++ * [assert(3)](http://man7.org/linux/man-pages/man3/assert.3.html) from ++ * `<assert.h>` instead, or the `__assert` and `__assert2` functions ++ * provided by bionic if more control is needed. They support automatically ++ * including the source filename and line number more conveniently than this ++ * function. ++ */ ++void __android_log_assert(const char* cond, const char* tag, const char* fmt, ...) ++ __attribute__((__noreturn__)) __attribute__((__format__(printf, 3, 4))); ++/** ++ * Identifies a specific log buffer for __android_log_buf_write() ++ * and __android_log_buf_print(). ++ */ ++typedef enum log_id { ++ LOG_ID_MIN = 0, ++ /** The main log buffer. This is the only log buffer available to apps. */ ++ LOG_ID_MAIN = 0, ++ /** The radio log buffer. */ ++ LOG_ID_RADIO = 1, ++ /** The event log buffer. */ ++ LOG_ID_EVENTS = 2, ++ /** The system log buffer. */ ++ LOG_ID_SYSTEM = 3, ++ /** The crash log buffer. */ ++ LOG_ID_CRASH = 4, ++ /** The statistics log buffer. */ ++ LOG_ID_STATS = 5, ++ /** The security log buffer. */ ++ LOG_ID_SECURITY = 6, ++ /** The kernel log buffer. */ ++ LOG_ID_KERNEL = 7, ++ LOG_ID_MAX, ++ /** Let the logging function choose the best log target. */ ++ LOG_ID_DEFAULT = 0x7FFFFFFF ++} log_id_t; ++/** ++ * Writes the constant string `text` to the log buffer `id`, ++ * with priority `prio` and tag `tag`. ++ * ++ * Apps should use __android_log_write() instead. ++ */ ++int __android_log_buf_write(int bufID, int prio, const char* tag, const char* text); ++/** ++ * Writes a formatted string to log buffer `id`, ++ * with priority `prio` and tag `tag`. ++ * The details of formatting are the same as for ++ * [printf(3)](http://man7.org/linux/man-pages/man3/printf.3.html). ++ * ++ * Apps should use __android_log_print() instead. ++ */ ++int __android_log_buf_print(int bufID, int prio, const char* tag, const char* fmt, ...) ++ __attribute__((__format__(printf, 4, 5))); ++/** ++ * Logger data struct used for writing log messages to liblog via __android_log_write_logger_data() ++ * and sending log messages to user defined loggers specified in __android_log_set_logger(). ++ */ ++struct __android_log_message { ++ size_t ++ struct_size; /** Must be set to sizeof(__android_log_message) and is used for versioning. */ ++ int32_t buffer_id; /** {@link log_id_t} values. */ ++ int32_t priority; /** {@link android_LogPriority} values. */ ++ const char* tag; /** The tag for the log message. */ ++ const char* file; /** Optional file name, may be set to nullptr. */ ++ uint32_t line; /** Optional line number, ignore if file is nullptr. */ ++ const char* message; /** The log message itself. */ ++}; ++/** ++ * Prototype for the 'logger' function that is called for every log message. ++ */ ++typedef void (*__android_logger_function)(const struct __android_log_message* log_message); ++/** ++ * Prototype for the 'abort' function that is called when liblog will abort due to ++ * __android_log_assert() failures. ++ */ ++typedef void (*__android_aborter_function)(const char* abort_message); ++#if !defined(__ANDROID__) || __ANDROID_API__ >= 30 ++/** ++ * Writes the log message specified by log_message. log_message includes additional file name and ++ * line number information that a logger may use. log_message is versioned for backwards ++ * compatibility. ++ * This assumes that loggability has already been checked through __android_log_is_loggable(). ++ * Higher level logging libraries, such as libbase, first check loggability, then format their ++ * buffers, then pass the message to liblog via this function, and therefore we do not want to ++ * duplicate the loggability check here. ++ * ++ * @param log_message the log message itself, see {@link __android_log_message}. ++ * ++ * Available since API level 30. ++ */ ++void __android_log_write_log_message(struct __android_log_message* log_message) __INTRODUCED_IN(30); ++/** ++ * Sets a user defined logger function. All log messages sent to liblog will be set to the ++ * function pointer specified by logger for processing. It is not expected that log messages are ++ * already terminated with a new line. This function should add new lines if required for line ++ * separation. ++ * ++ * @param logger the new function that will handle log messages. ++ * ++ * Available since API level 30. ++ */ ++void __android_log_set_logger(__android_logger_function logger) __INTRODUCED_IN(30); ++/** ++ * Writes the log message to logd. This is an __android_logger_function and can be provided to ++ * __android_log_set_logger(). It is the default logger when running liblog on a device. ++ * ++ * @param log_message the log message to write, see {@link __android_log_message}. ++ * ++ * Available since API level 30. ++ */ ++void __android_log_logd_logger(const struct __android_log_message* log_message) __INTRODUCED_IN(30); ++/** ++ * Writes the log message to stderr. This is an __android_logger_function and can be provided to ++ * __android_log_set_logger(). It is the default logger when running liblog on host. ++ * ++ * @param log_message the log message to write, see {@link __android_log_message}. ++ * ++ * Available since API level 30. ++ */ ++void __android_log_stderr_logger(const struct __android_log_message* log_message) ++ __INTRODUCED_IN(30); ++/** ++ * Sets a user defined aborter function that is called for __android_log_assert() failures. This ++ * user defined aborter function is highly recommended to abort and be noreturn, but is not strictly ++ * required to. ++ * ++ * @param aborter the new aborter function, see {@link __android_aborter_function}. ++ * ++ * Available since API level 30. ++ */ ++void __android_log_set_aborter(__android_aborter_function aborter) __INTRODUCED_IN(30); ++/** ++ * Calls the stored aborter function. This allows for other logging libraries to use the same ++ * aborter function by calling this function in liblog. ++ * ++ * @param abort_message an additional message supplied when aborting, for example this is used to ++ * call android_set_abort_message() in __android_log_default_aborter(). ++ * ++ * Available since API level 30. ++ */ ++void __android_log_call_aborter(const char* abort_message) __INTRODUCED_IN(30); ++/** ++ * Sets android_set_abort_message() on device then aborts(). This is the default aborter. ++ * ++ * @param abort_message an additional message supplied when aborting. This functions calls ++ * android_set_abort_message() with its contents. ++ * ++ * Available since API level 30. ++ */ ++void __android_log_default_aborter(const char* abort_message) __attribute__((noreturn)) ++__INTRODUCED_IN(30); ++/** ++ * Use the per-tag properties "log.tag." along with the minimum priority from ++ * __android_log_set_minimum_priority() to determine if a log message with a given prio and tag will ++ * be printed. A non-zero result indicates yes, zero indicates false. ++ * ++ * If both a priority for a tag and a minimum priority are set by ++ * __android_log_set_minimum_priority(), then the lowest of the two values are to determine the ++ * minimum priority needed to log. If only one is set, then that value is used to determine the ++ * minimum priority needed. If none are set, then default_priority is used. ++ * ++ * @param prio the priority to test, takes {@link android_LogPriority} values. ++ * @param tag the tag to test. ++ * @param len the length of the tag. ++ * @param default_prio the default priority to use if no properties or minimum priority are set. ++ * @return an integer where 1 indicates that the message is loggable and 0 indicates that it is not. ++ * ++ * Available since API level 30. ++ */ ++int __android_log_is_loggable(int prio, const char* tag, int default_prio) __INTRODUCED_IN(30); ++int __android_log_is_loggable_len(int prio, const char* tag, size_t len, int default_prio) ++ __INTRODUCED_IN(30); ++/** ++ * Sets the minimum priority that will be logged for this process. ++ * ++ * @param priority the new minimum priority to set, takes @{link android_LogPriority} values. ++ * @return the previous set minimum priority as @{link android_LogPriority} values, or ++ * ANDROID_LOG_DEFAULT if none was set. ++ * ++ * Available since API level 30. ++ */ ++int32_t __android_log_set_minimum_priority(int32_t priority) __INTRODUCED_IN(30); ++/** ++ * Gets the minimum priority that will be logged for this process. If none has been set by a ++ * previous __android_log_set_minimum_priority() call, this returns ANDROID_LOG_DEFAULT. ++ * ++ * @return the current minimum priority as @{link android_LogPriority} values, or ++ * ANDROID_LOG_DEFAULT if none is set. ++ * ++ * Available since API level 30. ++ */ ++int32_t __android_log_get_minimum_priority(void) __INTRODUCED_IN(30); ++/** ++ * Sets the default tag if no tag is provided when writing a log message. Defaults to ++ * getprogname(). This truncates tag to the maximum log message size, though appropriate tags ++ * should be much smaller. ++ * ++ * @param tag the new log tag. ++ * ++ * Available since API level 30. ++ */ ++void __android_log_set_default_tag(const char* tag) __INTRODUCED_IN(30); ++#endif ++#ifdef __cplusplus ++} ++#endif ++/** @} */ From 35af0786c0f5fbcfe7713c443c645cc2a09efb54 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 10 Sep 2022 21:55:26 +0100 Subject: [PATCH 02/11] nix: fix syntax error --- flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 1b1d494f57..3098f2ade8 100644 --- a/flake.nix +++ b/flake.nix @@ -26,7 +26,7 @@ sha256map = import ./scripts/nix/sha256map.nix; modules = [{ packages.direct-sqlcipher.patches = [ - "./scripts/nix/direct-sqlcipher-2.3.27.patch", + "./scripts/nix/direct-sqlcipher-2.3.27.patch" "./scripts/nix/direct-sqlcipher-android.patch" ]; packages.entropy.patches = [ ./scripts/nix/entropy.patch ]; From 9d70cf1e7bf99b690dbe8c369d41d2e4f604cfa2 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 10 Sep 2022 22:10:26 +0100 Subject: [PATCH 03/11] nix: update patches --- flake.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flake.nix b/flake.nix index 3098f2ade8..056735f4a4 100644 --- a/flake.nix +++ b/flake.nix @@ -26,8 +26,8 @@ sha256map = import ./scripts/nix/sha256map.nix; modules = [{ packages.direct-sqlcipher.patches = [ - "./scripts/nix/direct-sqlcipher-2.3.27.patch" - "./scripts/nix/direct-sqlcipher-android.patch" + ./scripts/nix/direct-sqlcipher-2.3.27.patch + ./scripts/nix/direct-sqlcipher-android.patch ]; packages.entropy.patches = [ ./scripts/nix/entropy.patch ]; } From f6e2f11299cf3b0efbc55509a09b73fba3440b19 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 10 Sep 2022 23:02:37 +0100 Subject: [PATCH 04/11] nix: patch to replace ERR_error_string with ERR_func_error_string --- flake.nix | 1 + scripts/nix/direct-sqlcipher-err-string.patch | 12 ++++++++++++ 2 files changed, 13 insertions(+) create mode 100644 scripts/nix/direct-sqlcipher-err-string.patch diff --git a/flake.nix b/flake.nix index 056735f4a4..0427d1ce7b 100644 --- a/flake.nix +++ b/flake.nix @@ -28,6 +28,7 @@ packages.direct-sqlcipher.patches = [ ./scripts/nix/direct-sqlcipher-2.3.27.patch ./scripts/nix/direct-sqlcipher-android.patch + ./scripts/nix/direct-sqlcipher-err-string.patch ]; packages.entropy.patches = [ ./scripts/nix/entropy.patch ]; } diff --git a/scripts/nix/direct-sqlcipher-err-string.patch b/scripts/nix/direct-sqlcipher-err-string.patch new file mode 100644 index 0000000000..c12c729acb --- /dev/null +++ b/scripts/nix/direct-sqlcipher-err-string.patch @@ -0,0 +1,12 @@ +diff --git a/cbits/sqlite3.c b/cbits/sqlite3.c +index 66bb609..ca32723 100644 +--- a/cbits/sqlite3.c ++++ b/cbits/sqlite3.c +@@ -105731,7 +105731,7 @@ static unsigned int openssl_init_count = 0; + static void sqlcipher_openssl_log_errors() { + unsigned long err = 0; + while((err = ERR_get_error()) != 0) { +- sqlcipher_log(SQLCIPHER_LOG_ERROR, "sqlcipher_openssl_log_errors: ERR_get_error() returned %lx: %s", err, ERR_error_string(err, NULL)); ++ sqlcipher_log(SQLCIPHER_LOG_ERROR, "sqlcipher_openssl_log_errors: ERR_get_error() returned %lx: %s", err, ERR_func_error_string(err)); + } + } From e080690c2e5ce9af51e60e146a8c4702880daa60 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 11 Sep 2022 09:23:16 +0100 Subject: [PATCH 05/11] nix: remove ERR_error_string patch --- flake.nix | 1 - scripts/nix/direct-sqlcipher-err-string.patch | 12 ------------ 2 files changed, 13 deletions(-) delete mode 100644 scripts/nix/direct-sqlcipher-err-string.patch diff --git a/flake.nix b/flake.nix index 0427d1ce7b..056735f4a4 100644 --- a/flake.nix +++ b/flake.nix @@ -28,7 +28,6 @@ packages.direct-sqlcipher.patches = [ ./scripts/nix/direct-sqlcipher-2.3.27.patch ./scripts/nix/direct-sqlcipher-android.patch - ./scripts/nix/direct-sqlcipher-err-string.patch ]; packages.entropy.patches = [ ./scripts/nix/entropy.patch ]; } diff --git a/scripts/nix/direct-sqlcipher-err-string.patch b/scripts/nix/direct-sqlcipher-err-string.patch deleted file mode 100644 index c12c729acb..0000000000 --- a/scripts/nix/direct-sqlcipher-err-string.patch +++ /dev/null @@ -1,12 +0,0 @@ -diff --git a/cbits/sqlite3.c b/cbits/sqlite3.c -index 66bb609..ca32723 100644 ---- a/cbits/sqlite3.c -+++ b/cbits/sqlite3.c -@@ -105731,7 +105731,7 @@ static unsigned int openssl_init_count = 0; - static void sqlcipher_openssl_log_errors() { - unsigned long err = 0; - while((err = ERR_get_error()) != 0) { -- sqlcipher_log(SQLCIPHER_LOG_ERROR, "sqlcipher_openssl_log_errors: ERR_get_error() returned %lx: %s", err, ERR_error_string(err, NULL)); -+ sqlcipher_log(SQLCIPHER_LOG_ERROR, "sqlcipher_openssl_log_errors: ERR_get_error() returned %lx: %s", err, ERR_func_error_string(err)); - } - } From 4be50fc9237670894b55708a1f8625fb3525582a Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 11 Sep 2022 09:38:40 +0100 Subject: [PATCH 06/11] nix: refactor ios parameters --- flake.nix | 69 ++++++++++++++++++++++++++----------------------------- 1 file changed, 33 insertions(+), 36 deletions(-) diff --git a/flake.nix b/flake.nix index 514292f758..a5e5329c72 100644 --- a/flake.nix +++ b/flake.nix @@ -65,6 +65,15 @@ echo "file binary-dist \"$(echo $out/*.zip)\"" \ > $out/nix-support/hydra-build-products ''; in + let iosOverrides = bundleName: { + smallAddressSpace = true; + enableShared = false; + # we need threaded here, otherwise all the queing logic doesn't work properly. + # for iOS we also use -staticlib, to get one rolled up library. + # still needs mac2ios patching of the archives. + ghcOptions = [ "-staticlib" "-threaded" "-DIOS" ]; + postInstall = iosPostInstall bundleName; + }; in rec { packages = { "lib:simplex-chat" = (drv pkgs).simplex-chat.components.library; @@ -235,45 +244,33 @@ ''; }; }; + + # builds for iOS and iOS simulator "aarch64-darwin" = { - # this is the aarch64-darwin iOS build (to be patched with mac2ios) - "aarch64-darwin-ios:lib:simplex-chat" = (drv' { pkgs' = pkgs; extra-modules = [{ packages.simplexmq.flags.swift = true; }]; } ).simplex-chat.components.library.override { - smallAddressSpace = true; enableShared = false; - # we need threaded here, otherwise all the queing logic doesn't work properly. - # for iOS we also use -staticlib, to get one rolled up library. - # still needs mac2ios patching of the archives. - ghcOptions = [ "-staticlib" "-threaded" "-DIOS" ]; - postInstall = iosPostInstall "pkg-ios-aarch64-swift-json"; - }; - # This is the aarch64-darwin build with tagged JSON format (for Mac & Flutter) - "aarch64-darwin:lib:simplex-chat" = (drv pkgs).simplex-chat.components.library.override { - smallAddressSpace = true; enableShared = false; - # we need threaded here, otherwise all the queing logic doesn't work properly. - # for iOS we also use -staticlib, to get one rolled up library. - # still needs mac2ios patching of the archives. - ghcOptions = [ "-staticlib" "-threaded" "-DIOS" ]; - postInstall = iosPostInstall "pkg-ios-aarch64-tagged-json"; - }; + # aarch64-darwin iOS build (to be patched with mac2ios) + "aarch64-darwin-ios:lib:simplex-chat" = (drv' { + pkgs' = pkgs; + extra-modules = [{ packages.simplexmq.flags.swift = true; }]; + }).simplex-chat.components.library.override ( + iosOverrides "pkg-ios-aarch64-swift-json" + ); + # aarch64-darwin build with tagged JSON format (for Mac & Flutter) + "aarch64-darwin:lib:simplex-chat" = (drv pkgs).simplex-chat.components.library.override ( + iosOverrides "pkg-ios-aarch64-tagged-json" + ); }; "x86_64-darwin" = { - # this is the aarch64-darwin iOS build (to be patched with mac2ios) - "x86_64-darwin-ios:lib:simplex-chat" = (drv' { pkgs' = pkgs; extra-modules = [{ packages.simplexmq.flags.swift = true; }]; } ).simplex-chat.components.library.override { - smallAddressSpace = true; enableShared = false; - # we need threaded here, otherwise all the queing logic doesn't work properly. - # for iOS we also use -staticlib, to get one rolled up library. - # still needs mac2ios patching of the archives. - ghcOptions = [ "-staticlib" "-threaded" "-DIOS" ]; - postInstall = iosPostInstall "pkg-ios-x86_64-swift-json"; - }; - # This is the aarch64-darwin build with tagged JSON format (for Mac & Flutter) - "x86_64-darwin:lib:simplex-chat" = (drv pkgs).simplex-chat.components.library.override { - smallAddressSpace = true; enableShared = false; - # we need threaded here, otherwise all the queing logic doesn't work properly. - # for iOS we also use -staticlib, to get one rolled up library. - # still needs mac2ios patching of the archives. - ghcOptions = [ "-staticlib" "-threaded" "-DIOS" ]; - postInstall = iosPostInstall "pkg-ios-x86_64-tagged-json"; - }; + # this is the x86_64-darwin iOS simulator build (to be patched with mac2ios) + "x86_64-darwin-ios:lib:simplex-chat" = (drv' { + pkgs' = pkgs; + extra-modules = [{ packages.simplexmq.flags.swift = true; }]; + }).simplex-chat.components.library.override ( + iosOverrides "pkg-ios-x86_64-swift-json" + ); + # This is the x86_64-darwin build with tagged JSON format (for Mac & Flutter iOS simulator) + "x86_64-darwin:lib:simplex-chat" = (drv pkgs).simplex-chat.components.library.override ( + iosOverrides "pkg-ios-x86_64-tagged-json" + ); }; }.${system} or {}); # build all packages in hydra. From 7aedd3d9e9008abc42ba5bfc956b12320bea7aaf Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 11 Sep 2022 19:07:29 +0100 Subject: [PATCH 07/11] inline files rfc (#1024) --- docs/rfcs/2022-09-06-send-small-files.md | 55 ++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 docs/rfcs/2022-09-06-send-small-files.md diff --git a/docs/rfcs/2022-09-06-send-small-files.md b/docs/rfcs/2022-09-06-send-small-files.md new file mode 100644 index 0000000000..221f851b6f --- /dev/null +++ b/docs/rfcs/2022-09-06-send-small-files.md @@ -0,0 +1,55 @@ +# Sending small files + +## Problem + +Sending files has a substantial constant overhead, and requires multiple online presenses from both sides, with additional connection handshake. For large files this overhead is justified, as otherwise files would consume queue quota, but for small files it makes sending files slow. + +## Solution + +Send small files in the same connection. There can be two modes of sending files - the one that requires explicit acceptance, but after the acceptance the file will be delivered inline, without creating a new conneciton. Another, when the file will be sent straight after the message - that would require preliminary agreement, per contact - this mode can be useful for small voice messages and gifs. + +## Design + +1. Add optional `fileInline :: Maybe FileInlineMode` property to `FileInvitation`: + +```haskell +data FileInlineMode + = FIInvitation -- recepient must accept + | FIChunks -- file is sent after the message without acceptance +``` + +2. Add `XFileAcptInline SharedMsgId String` message to accept inline files, this can only be sent in case inline mode is offered, so the sender would support it: + +``` +{ + "properties": { + "type": {"enum": ["x.file.acpt.inline"]}, + "msgId": {"ref": "base64url"}, + "params": { + "properties": { + "msgId": {"ref": "base64url"}, + "fileName": {"type": "string"} + } + } + } +} +``` + +3. Add `XFileChunks` message that have to be sent in front of the sequence of chunks (sent only in `FIInvitation` mode): + +``` +{ + "properties": { + "type": {"enum": ["x.file.chunks"]}, + "msgId": {"ref": "base64url"}, + "params": { + "properties": { + "msgId": {"ref": "base64url"}, + "fileName": {"type": "string"} + } + } + } +} +``` + +4. Support file chunks in the main connection if the previous message was `XFileChunks` or `FileInvitation` in `FIChunks` mode. From 9ff7dc8977730aa24571fd84db16d964eda134a8 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 12 Sep 2022 18:33:34 +0100 Subject: [PATCH 08/11] Typescript chat client SDK (#1036) * add typescript chat commands * add chat responses * add client API * fix chat bot example, readme * update readme * update main readme * readme * corrections Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com> * correction Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com> Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com> --- README.md | 10 +- .../simplex-chat-client/typescript/README.md | 38 +- .../typescript/examples/squaring-bot.js | 7 +- .../typescript/src/client.ts | 132 +++++- .../typescript/src/command.ts | 265 ++++++++++- .../typescript/src/response.ts | 429 +++++++++++++++++- 6 files changed, 834 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 6cc3b5bb77..922517a4e4 100644 --- a/README.md +++ b/README.md @@ -148,12 +148,12 @@ We plan to add soon: ## For developers -We plan that the SimpleX platform will grow into the platform supporting any distributed Internet application. This will allow you to build any service that people can access via chat, with custom web-based UI widgets that anybody with basic HTML/CSS/JavaScript knowledge can create in a few hours. +You can: -You already can: - -- use SimpleX Chat library to integrate chat functionality into your apps. -- use SimpleX Chat bot templates in Haskell to build your own chat bot services (TypeScript SDK is coming soon). +- use SimpleX Chat library to integrate chat functionality into your mobile apps. +- create chat bots and services in Haskell - see [simple](./apps/simplex-bot/) and more [advanced chat bot example](./apps/simplex-bot-advanced/). +- create chat bots and services in any language running SimpleX Chat terminal CLI as a local WebSocket server. See [TypeScript SimpleX Chat client](./packages/simplex-chat-client/) and [JavaScipt chat bot example](./packages/simplex-chat-client/typescript/examples/squaring-bot.js). +- run [simplex-chat terminal CLI](./docs/CLI.md) to execute individual chat commands, e.g. to send messages as part of shell script execution. If you are considering developing with SimpleX platform please get in touch for any advice and support. diff --git a/packages/simplex-chat-client/typescript/README.md b/packages/simplex-chat-client/typescript/README.md index 2aacf3ba01..2228d51648 100644 --- a/packages/simplex-chat-client/typescript/README.md +++ b/packages/simplex-chat-client/typescript/README.md @@ -1,23 +1,43 @@ # SimpleX Chat JavaScript client +This is a TypeScript library that defines WebSocket API client for [SimpleX Chat terminal CLI](https://github.com/simplex-chat/simplex-chat/blob/stable/docs/CLI.md) that should be run as a WebSockets server on any port: + +```bash +simplex-chat -p 5225 +``` + +Client API provides types and functions to: + +- create and change user profile (although, in most cases you can do it manually, via SimpleX Chat terminal app). +- create and accept invitations or connect with the contacts. +- create and manage long-term user address, accepting connection requests automatically. +- create, join and manage group. +- send and receive files. + +## Use cases + +- chat bots: you can implement any logic of connecting with and communicating with SimpleX Chat users. Using chat groups a chat bot can connect SimleX Chat users with each other. +- control of the equipment: e.g. servers or home automation. SimpleX Chat provides secure and authorised connections, so this is more secure than using rest APIs. + +Please share your use cases and implementations. + ## Quick start ``` npm i simplex-chat +npm run build ``` -See example of chat bot in [squaring-bot.js](./examples/squaring-bot.js) +See the example of a simple chat bot in [squaring-bot.js](./examples/squaring-bot.js): + +- start `simplex-chat` as a server on port 5225: `simplex-chat -p 5225 -d test_db` +- run chatbot: `node examples/squaring-bot` +- connect to chatbot via SimpleX Chat client using the address of the chat bot ## Documentation -Please refer to: +Please refer to the available client API in [client.ts](./src/client.ts). -- available client API - [client.ts](./src/client.ts). -- available commands - `ChatCommand` type in [command.ts](./src/command.ts) - if some command is not created as a ChatClient method, you can pass any command object to `sendChatCommand` method, or if the type for some command is not available you can pass command string (same strings as supported in terminal/mobile API) to `sendChatCmdStr` method. -- available chat messages - `ChatResponse` type in [response.ts](./src/command.ts). - -**Please note**: you should NOT use local display names that are supported in terminal app, as they can change when contact profile is updated and you can have race conditions - use commands that use chat IDs. - -## Lisense +## License [AGPL v3](./LICENSE) diff --git a/packages/simplex-chat-client/typescript/examples/squaring-bot.js b/packages/simplex-chat-client/typescript/examples/squaring-bot.js index 79b6dae709..8f874b2f46 100644 --- a/packages/simplex-chat-client/typescript/examples/squaring-bot.js +++ b/packages/simplex-chat-client/typescript/examples/squaring-bot.js @@ -6,15 +6,18 @@ run() async function run() { const chat = await ChatClient.create("ws://localhost:5225") + // this example assumes that you have initialized user profile for chat bot via terminal CLI const user = await chat.apiGetActiveUser() if (!user) { console.log("no user profile") return } console.log(`Bot profile: ${user.profile.displayName} (${user.profile.fullName})`) + // creates or uses the existing long-term address for the bot const address = (await chat.apiGetUserAddress()) || (await chat.apiCreateUserAddress()) console.log(`Bot address: ${address}`) - await chat.sendChatCmdStr("/auto_accept on") + // enables automatic acceptance of contact connections + await chat.addressAutoAccept(true) await processMessages(chat) async function processMessages(chat) { @@ -22,6 +25,7 @@ async function run() { const resp = r instanceof Promise ? await r : r switch (resp.type) { case "contactConnected": { + // sends welcome message when the new contact is connected const {contact} = resp console.log(`${contact.profile.displayName} connected`) await chat.apiSendTextMessage( @@ -32,6 +36,7 @@ async function run() { continue } case "newChatItem": { + // calculates the square of the number and sends the reply const {chatInfo} = resp.chatItem if (chatInfo.type !== ChatInfoType.Direct) continue const msg = ciContentText(resp.chatItem.chatItem.content) diff --git a/packages/simplex-chat-client/typescript/src/client.ts b/packages/simplex-chat-client/typescript/src/client.ts index 825a081429..3ed1ed5b68 100644 --- a/packages/simplex-chat-client/typescript/src/client.ts +++ b/packages/simplex-chat-client/typescript/src/client.ts @@ -1,7 +1,7 @@ import {ABQueue} from "./queue" import {ChatTransport, ChatServer, ChatSrvRequest, ChatSrvResponse, ChatResponseError, localServer, noop} from "./transport" -import {ChatCommand, ChatType} from "./command" -import {ChatResponse} from "./response" +import {ChatCommand, ChatType, Profile} from "./command" +import {ChatResponse, ChatInfo} from "./response" import * as CC from "./command" import * as CR from "./response" @@ -118,14 +118,37 @@ export class ChatClient { } } + async apiStopChat(): Promise { + const r = await this.sendChatCommand({type: "apiStopChat"}) + if (r.type !== "chatStopped") { + throw new ChatCommandError("error stopping chat", r) + } + } + + apiSetIncognito(incognito: boolean): Promise { + return this.okChatCommand({type: "setIncognito", incognito}) + } + + async addressAutoAccept(autoAccept: boolean, autoReply: CC.MsgContent): Promise { + const r = await this.sendChatCommand({type: "addressAutoAccept", autoAccept, autoReply}) + if (r.type !== "userContactLinkUpdated") { + throw new ChatCommandError("error changing user contact address mode", r) + } + } + async apiGetChats(): Promise { const r = await this.sendChatCommand({type: "apiGetChats"}) if (r.type === "apiChats") return r.chats throw new ChatCommandError("error loading chats", r) } - async apiGetChat(chatType: ChatType, chatId: number, pagination: CC.ChatPagination = {count: 100}): Promise { - const r = await this.sendChatCommand({type: "apiGetChat", chatType, chatId, pagination}) + async apiGetChat( + chatType: ChatType, + chatId: number, + pagination: CC.ChatPagination = {count: 100}, + search: string | undefined = undefined + ): Promise { + const r = await this.sendChatCommand({type: "apiGetChat", chatType, chatId, pagination, search}) if (r.type === "apiChat") return r.chat throw new ChatCommandError("error loading chat", r) } @@ -152,18 +175,6 @@ export class ChatClient { throw new ChatCommandError("error deleting chat item", r) } - // func getUserSMPServers() throws -> [String] { - // let r = chatSendCmdSync(.getUserSMPServers) - // if case let .userSMPServers(smpServers) = r { return smpServers } - // throw r - // } - - // func setUserSMPServers(smpServers: [String]) async throws { - // let r = await chatSendCmd(.setUserSMPServers(smpServers: smpServers)) - // if case .cmdOk = r { return } - // throw r - // } - async apiCreateLink(): Promise { const r = await this.sendChatCommand({type: "addContact"}) if (r.type === "invitation") return r.connReqInvitation @@ -184,7 +195,24 @@ export class ChatClient { async apiDeleteChat(chatType: ChatType, chatId: number): Promise { const r = await this.sendChatCommand({type: "apiDeleteChat", chatType, chatId}) - if (r.type !== "contactDeleted") throw new ChatCommandError("error deleting chat", r) + switch (chatType) { + case ChatType.Direct: + if (r.type === "contactDeleted") return + break + case ChatType.Group: + if (r.type === "groupDeletedUser") return + break + case ChatType.ContactRequest: + if (r.type === "contactConnectionDeleted") return + break + } + throw new ChatCommandError("error deleting chat", r) + } + + async apiClearChat(chatType: ChatType, chatId: number): Promise { + const r = await this.sendChatCommand({type: "apiClearChat", chatType, chatId}) + if (r.type === "chatCleared") return r.chatInfo + throw new ChatCommandError("error clearing chat", r) } async apiUpdateProfile(profile: CC.Profile): Promise { @@ -199,11 +227,11 @@ export class ChatClient { } } - // func apiParseMarkdown(text: String) throws -> [FormattedText]? { - // let r = chatSendCmdSync(.apiParseMarkdown(text: text)) - // if case let .apiParsedMarkdown(formattedText) = r { return formattedText } - // throw r - // } + async apiSetContactAlias(contactId: number, localAlias: string): Promise { + const r = await this.sendChatCommand({type: "apiSetContactAlias", contactId, localAlias}) + if (r.type === "contactAliasUpdated") return r.toContact + throw new ChatCommandError("error updating contact alias", r) + } async apiCreateUserAddress(): Promise { const r = await this.sendChatCommand({type: "createMyAddress"}) @@ -246,6 +274,66 @@ export class ChatClient { return this.okChatCommand({type: "apiChatRead", chatType, chatId, itemRange}) } + async apiContactInfo(contactId: number): Promise<[CR.ConnectionStats?, Profile?]> { + const r = await this.sendChatCommand({type: "apiContactInfo", contactId}) + if (r.type === "contactInfo") return [r.connectionStats, r.customUserProfile] + throw new ChatCommandError("error getting contact info", r) + } + + async apiGroupMemberInfo(groupId: number, memberId: number): Promise { + const r = await this.sendChatCommand({type: "apiGroupMemberInfo", groupId, memberId}) + if (r.type === "groupMemberInfo") return r.connectionStats_ + throw new ChatCommandError("error getting group info", r) + } + + async apiReceiveFile(fileId: number): Promise { + const r = await this.sendChatCommand({type: "receiveFile", fileId}) + if (r.type === "rcvFileAccepted") return r.chatItem + throw new ChatCommandError("error receiving file", r) + } + + async apiNewGroup(groupProfile: CR.GroupProfile): Promise { + const r = await this.sendChatCommand({type: "newGroup", groupProfile}) + if (r.type === "groupCreated") return r.groupInfo + throw new ChatCommandError("error creating group", r) + } + + async apiAddMember(groupId: number, contactId: number, memberRole: CC.GroupMemberRole): Promise { + const r = await this.sendChatCommand({type: "apiAddMember", groupId, contactId, memberRole}) + if (r.type === "sentGroupInvitation") return r.member + throw new ChatCommandError("error adding member", r) + } + + async apiJoinGroup(groupId: number): Promise { + const r = await this.sendChatCommand({type: "apiJoinGroup", groupId}) + if (r.type === "userAcceptedGroupSent") return r.groupInfo + throw new ChatCommandError("error joining group", r) + } + + async apiRemoveMember(groupId: number, memberId: number): Promise { + const r = await this.sendChatCommand({type: "apiRemoveMember", groupId, memberId}) + if (r.type === "userDeletedMember") return r.member + throw new ChatCommandError("error removing member", r) + } + + async apiLeaveGroup(groupId: number): Promise { + const r = await this.sendChatCommand({type: "apiLeaveGroup", groupId}) + if (r.type === "leftMemberUser") return r.groupInfo + throw new ChatCommandError("error leaving group", r) + } + + async apiListMembers(groupId: number): Promise { + const r = await this.sendChatCommand({type: "apiListMembers", groupId}) + if (r.type === "groupMembers") return r.group.members + throw new ChatCommandError("error getting group members", r) + } + + async apiUpdateGroup(groupId: number, groupProfile: CR.GroupProfile): Promise { + const r = await this.sendChatCommand({type: "apiUpdateGroupProfile", groupId, groupProfile}) + if (r.type === "groupUpdated") return r.toGroup + throw new ChatCommandError("error updating group", r) + } + private async okChatCommand(command: ChatCommand): Promise { const r = await this.sendChatCommand(command) if (r.type !== "cmdOk") throw new ChatCommandError(`${command.type} command error`, r) diff --git a/packages/simplex-chat-client/typescript/src/command.ts b/packages/simplex-chat-client/typescript/src/command.ts index 3266bf3456..241ce2f0e9 100644 --- a/packages/simplex-chat-client/typescript/src/command.ts +++ b/packages/simplex-chat-client/typescript/src/command.ts @@ -2,7 +2,12 @@ export type ChatCommand = | ShowActiveUser | CreateActiveUser | StartChat + | APIStopChat | SetFilesFolder + | SetIncognito + | APIExportArchive + | APIImportArchive + | APIDeleteStorage | APIGetChats | APIGetChat | APISendMessage @@ -10,12 +15,23 @@ export type ChatCommand = | APIDeleteChatItem | APIChatRead | APIDeleteChat + | APIClearChat | APIAcceptContact | APIRejectContact | APIUpdateProfile + | APISetContactAlias | APIParseMarkdown + | NewGroup + | APIAddMember + | APIJoinGroup + | APIRemoveMember + | APILeaveGroup + | APIListMembers + | APIUpdateGroupProfile | GetUserSMPServers | SetUserSMPServers + | APIContactInfo + | APIGroupMemberInfo | AddContact | Connect | ConnectSimplex @@ -23,12 +39,48 @@ export type ChatCommand = | DeleteMyAddress | ShowMyAddress | AddressAutoAccept + | ReceiveFile + | CancelFile + | FileStatus + +// not included commands (they are not needed for Websocket clients, and can still be sent as strings): +// APIActivateChat +// APISuspendChat +// ResubscribeAllConnections +// APIGetChatItems - not implemented +// APISendCallInvitation +// APIRejectCall +// APISendCallOffer +// APISendCallAnswer +// APISendCallExtraInfo +// APIEndCall +// APIGetCallInvitations +// APICallStatus +// APIGetNtfToken +// APIRegisterToken +// APIVerifyToken +// APIDeleteToken +// APIGetNtfMessage +// APIMemberRole -- not implemented +// ListContacts +// ListGroups +// APISetNetworkConfig +// APIGetNetworkConfig +// APISetChatSettings +// ShowMessages +// LastMessages +// SendMessageBroadcast type ChatCommandTag = | "showActiveUser" | "createActiveUser" | "startChat" + | "apiStopChat" | "setFilesFolder" + | "setIncognito" + | "apiExportArchive" + | "apiImportArchive" + | "apiDeleteStorage" | "apiGetChats" | "apiGetChat" | "apiSendMessage" @@ -36,12 +88,23 @@ type ChatCommandTag = | "apiDeleteChatItem" | "apiChatRead" | "apiDeleteChat" + | "apiClearChat" | "apiAcceptContact" | "apiRejectContact" | "apiUpdateProfile" + | "apiSetContactAlias" | "apiParseMarkdown" + | "newGroup" + | "apiAddMember" + | "apiJoinGroup" + | "apiRemoveMember" + | "apiLeaveGroup" + | "apiListMembers" + | "apiUpdateGroupProfile" | "getUserSMPServers" | "setUserSMPServers" + | "apiContactInfo" + | "apiGroupMemberInfo" | "addContact" | "connect" | "connectSimplex" @@ -49,6 +112,9 @@ type ChatCommandTag = | "deleteMyAddress" | "showMyAddress" | "addressAutoAccept" + | "receiveFile" + | "cancelFile" + | "fileStatus" interface IChatCommand { type: ChatCommandTag @@ -65,6 +131,11 @@ export interface CreateActiveUser extends IChatCommand { export interface StartChat extends IChatCommand { type: "startChat" + subscribeConnections?: boolean +} + +export interface APIStopChat extends IChatCommand { + type: "apiStopChat" } export interface SetFilesFolder extends IChatCommand { @@ -72,8 +143,28 @@ export interface SetFilesFolder extends IChatCommand { filePath: string } +export interface SetIncognito extends IChatCommand { + type: "setIncognito" + incognito: boolean +} + +export interface APIExportArchive extends IChatCommand { + type: "apiExportArchive" + config: ArchiveConfig +} + +export interface APIImportArchive extends IChatCommand { + type: "apiImportArchive" + config: ArchiveConfig +} + +export interface APIDeleteStorage extends IChatCommand { + type: "apiDeleteStorage" +} + export interface APIGetChats extends IChatCommand { type: "apiGetChats" + pendingConnections?: boolean } export interface APIGetChat extends IChatCommand { @@ -81,6 +172,7 @@ export interface APIGetChat extends IChatCommand { chatType: ChatType chatId: number pagination: ChatPagination + search?: string } export interface APISendMessage extends IChatCommand { @@ -130,6 +222,12 @@ export interface APIDeleteChat extends IChatCommand { chatId: number } +export interface APIClearChat extends IChatCommand { + type: "apiClearChat" + chatType: ChatType + chatId: number +} + export interface APIAcceptContact extends IChatCommand { type: "apiAcceptContact" contactReqId: number @@ -145,11 +243,56 @@ export interface APIUpdateProfile extends IChatCommand { profile: Profile } +export interface APISetContactAlias extends IChatCommand { + type: "apiSetContactAlias" + contactId: number + localAlias: string +} + export interface APIParseMarkdown extends IChatCommand { type: "apiParseMarkdown" text: string } +export interface NewGroup extends IChatCommand { + type: "newGroup" + groupProfile: GroupProfile +} + +export interface APIAddMember extends IChatCommand { + type: "apiAddMember" + groupId: number + contactId: number + memberRole: GroupMemberRole +} + +export interface APIJoinGroup extends IChatCommand { + type: "apiJoinGroup" + groupId: number +} + +export interface APIRemoveMember extends IChatCommand { + type: "apiRemoveMember" + groupId: number + memberId: number +} + +export interface APILeaveGroup extends IChatCommand { + type: "apiLeaveGroup" + groupId: number +} + +export interface APIListMembers extends IChatCommand { + type: "apiListMembers" + groupId: number +} + +export interface APIUpdateGroupProfile extends IChatCommand { + type: "apiUpdateGroupProfile" + groupId: number + groupProfile: GroupProfile +} + export interface GetUserSMPServers extends IChatCommand { type: "getUserSMPServers" } @@ -159,6 +302,17 @@ export interface SetUserSMPServers extends IChatCommand { servers: [string] } +export interface APIContactInfo extends IChatCommand { + type: "apiContactInfo" + contactId: number +} + +export interface APIGroupMemberInfo extends IChatCommand { + type: "apiGroupMemberInfo" + groupId: number + memberId: number +} + export interface AddContact extends IChatCommand { type: "addContact" } @@ -186,7 +340,24 @@ export interface ShowMyAddress extends IChatCommand { export interface AddressAutoAccept extends IChatCommand { type: "addressAutoAccept" - enable: boolean + autoAccept: boolean + autoReply?: MsgContent +} + +export interface ReceiveFile extends IChatCommand { + type: "receiveFile" + fileId: number + filePath?: string +} + +export interface CancelFile extends IChatCommand { + type: "cancelFile" + fileId: number +} + +export interface FileStatus extends IChatCommand { + type: "fileStatus" + fileId: number } export interface Profile { @@ -208,12 +379,13 @@ export type ChatPagination = export type ChatItemId = number -type MsgContentTag = "text" | "link" | "images" +type MsgContentTag = "text" | "link" | "image" | "file" -export type MsgContent = MCText | MCUnknown +export type MsgContent = MCText | MCLink | MCImage | MCFile | MCUnknown interface MC { type: MsgContentTag + text: string } interface MCText extends MC { @@ -221,9 +393,32 @@ interface MCText extends MC { text: string } +interface MCLink extends MC { + type: "link" + text: string + preview: LinkPreview +} + +interface MCImage extends MC { + type: "image" + image: string // image preview as base64 encoded data string +} + +interface MCFile extends MC { + type: "file" + text: string +} + interface MCUnknown { type: string - text?: string + text: string +} + +interface LinkPreview { + uri: string + title: string + description: string + image: string } export enum DeleteMode { @@ -231,6 +426,24 @@ export enum DeleteMode { Internal = "internal", } +interface ArchiveConfig { + archivePath: string + disableCompression?: boolean + parentTempDirectory?: string +} + +export enum GroupMemberRole { + GRMember = "member", + GRAdmin = "admin", + GROwner = "owner", +} + +interface GroupProfile { + displayName: string + fullName: string // can be empty string + image?: string +} + export function cmdString(cmd: ChatCommand): string { switch (cmd.type) { case "showActiveUser": @@ -238,11 +451,21 @@ export function cmdString(cmd: ChatCommand): string { case "createActiveUser": return `/u ${JSON.stringify(cmd.profile)}` case "startChat": - return "/_start" + return `/_start subscribe=${cmd.subscribeConnections ? "on" : "off"}` + case "apiStopChat": + return "/_stop" case "setFilesFolder": return `/_files_folder ${cmd.filePath}` + case "setIncognito": + return `/incognito ${cmd.incognito ? "on" : "off"}` + case "apiExportArchive": + return `/_db export ${JSON.stringify(cmd.config)}` + case "apiImportArchive": + return `/_db import ${JSON.stringify(cmd.config)}` + case "apiDeleteStorage": + return "/_db delete" case "apiGetChats": - return "/_get chats" + return `/_get chats pcc=${cmd.pendingConnections ? "on" : "off"}` case "apiGetChat": return `/_get chat ${cmd.chatType}${cmd.chatId}${paginationStr(cmd.pagination)}` case "apiSendMessage": @@ -257,18 +480,40 @@ export function cmdString(cmd: ChatCommand): string { } case "apiDeleteChat": return `/_delete ${cmd.chatType}${cmd.chatId}` + case "apiClearChat": + return `/_clear chat ${cmd.chatType}${cmd.chatId}` case "apiAcceptContact": return `/_accept ${cmd.contactReqId}` case "apiRejectContact": return `/_reject ${cmd.contactReqId}` case "apiUpdateProfile": return `/_profile ${JSON.stringify(cmd.profile)}` + case "apiSetContactAlias": + return `/_set alias @${cmd.contactId} ${cmd.localAlias.trim()}` case "apiParseMarkdown": return `/_parse ${cmd.text}` + case "newGroup": + return `/_group ${JSON.stringify(cmd.groupProfile)}` + case "apiAddMember": + return `/_add #${cmd.groupId} ${cmd.contactId} ${cmd.memberRole}` + case "apiJoinGroup": + return `/_join #${cmd.groupId}` + case "apiRemoveMember": + return `/_remove #${cmd.groupId} ${cmd.memberId}` + case "apiLeaveGroup": + return `/_leave #${cmd.groupId}` + case "apiListMembers": + return `/_members #${cmd.groupId}` + case "apiUpdateGroupProfile": + return `/_group_profile #${cmd.groupId} ${JSON.stringify(cmd.groupProfile)}` case "getUserSMPServers": return "/smp_servers" case "setUserSMPServers": return `/smp_servers ${cmd.servers.join(",") || "default"}` + case "apiContactInfo": + return `/_info @${cmd.contactId}` + case "apiGroupMemberInfo": + return `/_info #${cmd.groupId} ${cmd.memberId}` case "addContact": return "/connect" case "connect": @@ -282,7 +527,13 @@ export function cmdString(cmd: ChatCommand): string { case "showMyAddress": return "/show_address" case "addressAutoAccept": - return `/auto_accept ${cmd.enable ? "on" : "off"}` + return `/auto_accept ${cmd.autoAccept ? "on" : "off"}${cmd.autoReply ? " " + JSON.stringify(cmd.autoReply) : ""}` + case "receiveFile": + return `/freceive ${cmd.fileId}${cmd.filePath ? " " + cmd.filePath : ""}` + case "cancelFile": + return `/fcancel ${cmd.fileId}` + case "fileStatus": + return `/fstatus ${cmd.fileId}` } } diff --git a/packages/simplex-chat-client/typescript/src/response.ts b/packages/simplex-chat-client/typescript/src/response.ts index 6b10e865bb..48fc921da1 100644 --- a/packages/simplex-chat-client/typescript/src/response.ts +++ b/packages/simplex-chat-client/typescript/src/response.ts @@ -1,13 +1,16 @@ -import {ChatItemId, MsgContent, DeleteMode, Profile} from "./command" +import {ChatItemId, MsgContent, DeleteMode, Profile, GroupMemberRole} from "./command" export type ChatResponse = | CRActiveUser | CRChatStarted | CRChatRunning + | CRChatStopped | CRApiChats | CRApiChat | CRApiParsedMarkdown | CRUserSMPServers + | CRContactInfo + | CRGroupMemberInfo | CRNewChatItem | CRChatItemStatusUpdated | CRChatItemUpdated @@ -20,11 +23,14 @@ export type ChatResponse = | CRUserProfile | CRUserProfileNoChange | CRUserProfileUpdated + | CRContactAliasUpdated | CRInvitation | CRSentConfirmation | CRSentInvitation | CRContactUpdated + | CRContactsMerged | CRContactDeleted + | CRChatCleared | CRUserContactLinkCreated | CRUserContactLinkDeleted | CRReceivedContactRequest @@ -38,22 +44,72 @@ export type ChatResponse = | CRContactSubscribed | CRContactSubError | CRContactSubSummary + | CRContactsDisconnected + | CRContactsSubscribed + | CRHostConnected + | CRHostDisconnected | CRGroupEmpty + | CRMemberSubError + | CRMemberSubSummary + | CRGroupSubscribed + | CRRcvFileAccepted + | CRRcvFileAcceptedSndCancelled + | CRRcvFileStart + | CRRcvFileComplete + | CRRcvFileCancelled + | CRRcvFileSndCancelled + | CRSndFileStart + | CRSndFileComplete + | CRSndFileCancelled + | CRSndFileRcvCancelled + | CRSndGroupFileCancelled + | CRSndFileSubError + | CRRcvFileSubError | CRPendingSubSummary + | CRGroupCreated + | CRGroupMembers + | CRUserAcceptedGroupSent + | CRUserDeletedMember + | CRSentGroupInvitation + | CRLeftMemberUser + | CRGroupDeletedUser + | CRGroupInvitation + | CRReceivedGroupInvitation + | CRUserJoinedGroup + | CRJoinedGroupMember + | CRJoinedGroupMemberConnecting + | CRConnectedToGroupMember + | CRDeletedMember + | CRDeletedMemberUser + | CRLeftMember + | CRGroupRemoved + | CRGroupDeleted + | CRGroupUpdated | CRUserContactLinkSubscribed | CRUserContactLinkSubError + | CRNewContactConnection + | CRContactConnectionDeleted | CRMessageError | CRChatCmdError | CRChatError +// not included +// CRChatItemDeletedNotFound +// CRBroadcastSent +// CRGroupsList +// CRFileTransferStatus + type ChatResponseTag = | "activeUser" | "chatStarted" | "chatRunning" + | "chatStopped" | "apiChats" | "apiChat" | "apiParsedMarkdown" | "userSMPServers" + | "contactInfo" + | "groupMemberInfo" | "newChatItem" | "chatItemStatusUpdated" | "chatItemUpdated" @@ -68,11 +124,14 @@ type ChatResponseTag = | "userProfile" | "userProfileNoChange" | "userProfileUpdated" + | "contactAliasUpdated" | "invitation" | "sentConfirmation" | "sentInvitation" | "contactUpdated" + | "contactsMerged" | "contactDeleted" + | "chatCleared" | "receivedContactRequest" | "acceptingContactRequest" | "contactAlreadyExists" @@ -84,10 +143,52 @@ type ChatResponseTag = | "contactSubscribed" | "contactSubError" | "contactSubSummary" + | "contactsDisconnected" + | "contactsSubscribed" + | "hostConnected" + | "hostDisconnected" | "groupEmpty" + | "memberSubError" + | "memberSubSummary" + | "groupSubscribed" + | "rcvFileAccepted" + | "rcvFileAcceptedSndCancelled" + | "rcvFileStart" + | "rcvFileComplete" + | "rcvFileCancelled" + | "rcvFileSndCancelled" + | "sndFileStart" + | "sndFileComplete" + | "sndFileCancelled" + | "sndFileRcvCancelled" + | "sndGroupFileCancelled" + | "fileTransferStatus" + | "sndFileSubError" + | "rcvFileSubError" | "pendingSubSummary" + | "groupCreated" + | "groupMembers" + | "userAcceptedGroupSent" + | "userDeletedMember" + | "sentGroupInvitation" + | "leftMemberUser" + | "groupDeletedUser" + | "groupInvitation" + | "receivedGroupInvitation" + | "userJoinedGroup" + | "joinedGroupMember" + | "joinedGroupMemberConnecting" + | "connectedToGroupMember" + | "deletedMember" + | "deletedMemberUser" + | "leftMember" + | "groupRemoved" + | "groupDeleted" + | "groupUpdated" | "userContactLinkSubscribed" | "userContactLinkSubError" + | "newContactConnection" + | "contactConnectionDeleted" | "messageError" | "chatCmdError" | "chatError" @@ -109,6 +210,10 @@ export interface CRChatRunning extends CR { type: "chatRunning" } +export interface CRChatStopped extends CR { + type: "chatStopped" +} + export interface CRApiChats extends CR { type: "apiChats" chats: Chat[] @@ -129,6 +234,20 @@ export interface CRUserSMPServers extends CR { smpServers: string[] } +export interface CRContactInfo extends CR { + type: "contactInfo" + contact: Contact + connectionStats: ConnectionStats + customUserProfile?: Profile +} + +export interface CRGroupMemberInfo extends CR { + type: "groupMemberInfo" + groupInfo: GroupInfo + member: GroupMember + connectionStats_?: ConnectionStats +} + export interface CRNewChatItem extends CR { type: "newChatItem" chatItem: AChatItem @@ -169,6 +288,7 @@ export interface CRUserContactLinkUpdated extends CR { type: "userContactLinkUpdated" connReqContact: string autoAccept: boolean + autoReply?: MsgContent } export interface CRContactRequestRejected extends CR { @@ -191,6 +311,11 @@ export interface CRUserProfileUpdated extends CR { toProfile: Profile } +export interface CRContactAliasUpdated extends CR { + type: "contactAliasUpdated" + toContact: Contact +} + export interface CRInvitation extends CR { type: "invitation" connReqInvitation: string @@ -210,11 +335,22 @@ export interface CRContactUpdated extends CR { toContact: Contact } +export interface CRContactsMerged extends CR { + type: "contactsMerged" + intoContact: Contact + mergedContact: Contact +} + export interface CRContactDeleted extends CR { type: "contactDeleted" contact: Contact } +export interface CRChatCleared extends CR { + type: "chatCleared" + chatInfo: ChatInfo +} + export interface CRUserContactLinkCreated extends CR { type: "userContactLinkCreated" connReqContact: string @@ -280,16 +416,242 @@ export interface CRContactSubSummary extends CR { contactSubscriptions: ContactSubStatus[] } +export interface CRContactsDisconnected extends CR { + type: "contactsDisconnected" + server: string + contactRefs: ContactRef[] +} + +export interface CRContactsSubscribed extends CR { + type: "contactsSubscribed" + server: string + contactRefs: ContactRef[] +} + +export interface CRHostConnected extends CR { + type: "hostConnected" + protocol: string + transportHost: string +} + +export interface CRHostDisconnected extends CR { + type: "hostDisconnected" + protocol: string + transportHost: string +} + export interface CRGroupEmpty extends CR { type: "groupEmpty" groupInfo: GroupInfo } +export interface CRMemberSubError extends CR { + type: "memberSubError" + groupInfo: GroupInfo + member: GroupMember + chatError: ChatError +} + +export interface CRMemberSubSummary extends CR { + type: "memberSubSummary" + memberSubscriptions: MemberSubStatus[] +} + +export interface CRGroupSubscribed extends CR { + type: "groupSubscribed" + groupInfo: GroupInfo +} + +export interface CRRcvFileAccepted extends CR { + type: "rcvFileAccepted" + chatItem: AChatItem +} + +export interface CRRcvFileAcceptedSndCancelled extends CR { + type: "rcvFileAcceptedSndCancelled" + rcvFileTransfer: RcvFileTransfer +} + +export interface CRRcvFileStart extends CR { + type: "rcvFileStart" + chatItem: AChatItem +} + +export interface CRRcvFileComplete extends CR { + type: "rcvFileComplete" + chatItem: AChatItem +} + +export interface CRRcvFileCancelled extends CR { + type: "rcvFileCancelled" + rcvFileTransfer: RcvFileTransfer +} + +export interface CRRcvFileSndCancelled extends CR { + type: "rcvFileSndCancelled" + rcvFileTransfer: RcvFileTransfer +} + +export interface CRSndFileStart extends CR { + type: "sndFileStart" + chatItem: AChatItem + sndFileTransfer: SndFileTransfer +} + +export interface CRSndFileComplete extends CR { + type: "sndFileComplete" + chatItem: AChatItem + sndFileTransfer: SndFileTransfer +} + +export interface CRSndFileCancelled extends CR { + type: "sndFileCancelled" + chatItem: AChatItem + sndFileTransfer: SndFileTransfer +} + +export interface CRSndFileRcvCancelled extends CR { + type: "sndFileRcvCancelled" + chatItem: AChatItem + sndFileTransfer: SndFileTransfer +} + +export interface CRSndGroupFileCancelled extends CR { + type: "sndGroupFileCancelled" + chatItem: AChatItem + fileTransferMeta: FileTransferMeta + sndFileTransfers: SndFileTransfer[] +} + +export interface CRSndFileSubError extends CR { + type: "sndFileSubError" + sndFileTransfer: SndFileTransfer + chatError: ChatError +} + +export interface CRRcvFileSubError extends CR { + type: "rcvFileSubError" + rcvFileTransfer: RcvFileTransfer + chatError: ChatError +} + export interface CRPendingSubSummary extends CR { type: "pendingSubSummary" pendingSubStatus: PendingSubStatus[] } +export interface CRGroupCreated extends CR { + type: "groupCreated" + groupInfo: GroupInfo +} + +export interface CRGroupMembers extends CR { + type: "groupMembers" + group: Group +} + +export interface CRUserAcceptedGroupSent extends CR { + type: "userAcceptedGroupSent" + groupInfo: GroupInfo +} + +export interface CRUserDeletedMember extends CR { + type: "userDeletedMember" + groupInfo: GroupInfo + member: GroupMember +} + +export interface CRSentGroupInvitation extends CR { + type: "sentGroupInvitation" + groupInfo: GroupInfo + contact: Contact + member: GroupMember +} + +export interface CRLeftMemberUser extends CR { + type: "leftMemberUser" + groupInfo: GroupInfo +} + +export interface CRGroupDeletedUser extends CR { + type: "groupDeletedUser" + groupInfo: GroupInfo +} + +export interface CRGroupInvitation extends CR { + type: "groupInvitation" + groupInfo: GroupInfo +} + +export interface CRReceivedGroupInvitation extends CR { + type: "receivedGroupInvitation" + groupInfo: GroupInfo + contact: Contact + memberRole: GroupMemberRole +} + +export interface CRUserJoinedGroup extends CR { + type: "userJoinedGroup" + groupInfo: GroupInfo + hostMember: GroupMember +} + +export interface CRJoinedGroupMember extends CR { + type: "joinedGroupMember" + groupInfo: GroupInfo + member: GroupMember +} + +export interface CRJoinedGroupMemberConnecting extends CR { + type: "joinedGroupMemberConnecting" + groupInfo: GroupInfo + hostMember: GroupMember + member: GroupMember +} + +export interface CRConnectedToGroupMember extends CR { + type: "connectedToGroupMember" + groupInfo: GroupInfo + member: GroupMember +} + +export interface CRDeletedMember extends CR { + type: "deletedMember" + groupInfo: GroupInfo + byMember: GroupMember + deletedMember: GroupMember +} + +export interface CRDeletedMemberUser extends CR { + type: "deletedMemberUser" + groupInfo: GroupInfo + member: GroupMember +} + +export interface CRLeftMember extends CR { + type: "leftMember" + groupInfo: GroupInfo + member: GroupMember +} + +export interface CRGroupRemoved extends CR { + type: "groupRemoved" + groupInfo: GroupInfo +} + +export interface CRGroupDeleted extends CR { + type: "groupDeleted" + groupInfo: GroupInfo + member: GroupMember +} + +export interface CRGroupUpdated extends CR { + type: "groupUpdated" + fromGroup: GroupInfo + toGroup: GroupInfo + member_?: GroupMember +} + export interface CRUserContactLinkSubscribed extends CR { type: "userContactLinkSubscribed" } @@ -299,6 +661,16 @@ export interface CRUserContactLinkSubError extends CR { chatError: ChatError } +export interface CRNewContactConnection extends CR { + type: "newContactConnection" + connection: PendingContactConnection +} + +export interface CRContactConnectionDeleted extends CR { + type: "contactConnectionDeleted" + connection: PendingContactConnection +} + export interface CRMessageError extends CR { type: "messageError" severity: string @@ -365,6 +737,16 @@ export interface Contact { createdAt: Date } +export interface ContactRef { + contactId: number + localDisplayName: string +} + +export interface Group { + groupInfo: GroupInfo + members: GroupMember[] +} + export interface GroupInfo { groupId: number localDisplayName: string @@ -382,7 +764,7 @@ export interface GroupProfile { export interface GroupMember { groupMemberId: number memberId: string - // memberRole: GroupMemberRole + memberRole: GroupMemberRole // memberCategory: GroupMemberCategory // memberStatus: GroupMemberStatus // invitedBy: InvitedBy @@ -498,7 +880,36 @@ export function ciContentText(content: CIContent): string | undefined { } } -interface RcvFileTransfer {} +interface RcvFileTransfer { + fileId: number + // fileInvitation: FileInvitation + // fileStatus: RcvFileStatus + senderDisplayName: string + chunkSize: number + cancelled: boolean + grpMemberId?: number +} + +interface SndFileTransfer { + fileId: number + fileName: string + filePath: string + fileSize: number + chunkSize: number + recipientDisplayName: string + connId: number + // agentConnId: string + // fileStatus: FileStatus +} + +interface FileTransferMeta { + fileId: number + fileName: string + filePath: string + fileSize: number + chunkSize: number + cancelled: boolean +} export interface ChatStats { unreadCount: number @@ -580,6 +991,18 @@ interface ContactSubStatus {} interface PendingSubStatus {} +export interface ConnectionStats { + rcvServers?: string[] + sndServers?: string[] +} + +interface PendingContactConnection {} + +interface MemberSubStatus { + member: GroupMember + memberError?: ChatError +} + interface AgentErrorType { type: string [x: string]: any From bf0fdf6d4225ca5400cb2cc12f9d3397b6ec8600 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Tue, 13 Sep 2022 00:47:44 +0300 Subject: [PATCH 09/11] android: animated images support (#1038) * Animated images support * Provide correct mime type when saving an image * Higher limit size for images auto download --- apps/android/app/build.gradle | 4 ++ .../java/chat/simplex/app/model/SimpleXAPI.kt | 2 +- .../simplex/app/views/chat/ComposeView.kt | 32 ++++++++---- .../app/views/chat/item/CIImageView.kt | 50 +++++++++++++++++-- .../views/chat/item/ImageFullScreenView.kt | 29 ++++++++++- .../chat/simplex/app/views/helpers/Share.kt | 11 +++- .../chat/simplex/app/views/helpers/Util.kt | 27 ++++++++++ 7 files changed, 137 insertions(+), 18 deletions(-) diff --git a/apps/android/app/build.gradle b/apps/android/app/build.gradle index eba863a3bf..1d95cfeddd 100644 --- a/apps/android/app/build.gradle +++ b/apps/android/app/build.gradle @@ -119,6 +119,10 @@ dependencies { // Biometric authentication implementation 'androidx.biometric:biometric:1.2.0-alpha04' + // GIFs support + implementation "io.coil-kt:coil-compose:2.1.0" + implementation "io.coil-kt:coil-gif:2.1.0" + testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index 3eec9ba191..10321ac912 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -777,7 +777,7 @@ open class ChatController(private val ctrl: ChatCtrl, val ntfManager: NtfManager val cItem = r.chatItem.chatItem chatModel.addChatItem(cInfo, cItem) val file = cItem.file - if (cItem.content.msgContent is MsgContent.MCImage && file != null && file.fileSize <= MAX_IMAGE_SIZE && appPrefs.privacyAcceptImages.get()) { + if (cItem.content.msgContent is MsgContent.MCImage && file != null && file.fileSize <= MAX_IMAGE_SIZE_AUTO_RCV && appPrefs.privacyAcceptImages.get()) { withApi { receiveFile(file.fileId) } } if (!cItem.chatDir.sent && !cItem.isCall && (!isAppOnForeground(appContext) || chatModel.chatId.value != cInfo.id)) { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt index d96b8a75ca..b3c5b73353 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt @@ -8,6 +8,7 @@ import android.content.* import android.content.pm.PackageManager import android.graphics.Bitmap import android.graphics.ImageDecoder +import android.graphics.drawable.AnimatedImageDrawable import android.net.Uri import android.provider.MediaStore import android.util.Log @@ -146,6 +147,7 @@ fun ComposeView( val textStyle = remember { mutableStateOf(smallFont) } // attachments val chosenImage = remember { mutableStateOf(null) } + val chosenAnimImage = remember { mutableStateOf(null) } val chosenFile = remember { mutableStateOf(null) } val photoUri = remember { mutableStateOf(null) } val photoTmpFile = remember { mutableStateOf(null) } @@ -194,24 +196,23 @@ fun ComposeView( Toast.makeText(context, generalGetString(R.string.toast_permission_denied), Toast.LENGTH_SHORT).show() } } - val galleryLauncher = rememberLauncherForActivityResult(contract = PickFromGallery()) { uri: Uri? -> + val processPickedImage = { uri: Uri? -> if (uri != null) { val source = ImageDecoder.createSource(context.contentResolver, uri) + val drawable = ImageDecoder.decodeDrawable(source) val bitmap = ImageDecoder.decodeBitmap(source) - chosenImage.value = bitmap - val imagePreview = resizeImageToStrSize(bitmap, maxDataSize = 14000) - composeState.value = composeState.value.copy(preview = ComposePreview.ImagePreview(imagePreview)) - } - } - val galleryLauncherFallback = rememberGetContentLauncher { uri: Uri? -> - if (uri != null) { - val source = ImageDecoder.createSource(context.contentResolver, uri) - val bitmap = ImageDecoder.decodeBitmap(source) - chosenImage.value = bitmap + if (drawable is AnimatedImageDrawable) { + // It's a gif or webp + chosenAnimImage.value = uri + } else { + chosenImage.value = bitmap + } val imagePreview = resizeImageToStrSize(bitmap, maxDataSize = 14000) composeState.value = composeState.value.copy(preview = ComposePreview.ImagePreview(imagePreview)) } } + val galleryLauncher = rememberLauncherForActivityResult(contract = PickFromGallery(), processPickedImage) + val galleryLauncherFallback = rememberGetContentLauncher(processPickedImage) val filesLauncher = rememberGetContentLauncher { uri: Uri? -> if (uri != null) { val fileSize = getFileSize(context, uri) @@ -334,6 +335,7 @@ fun ComposeView( composeState.value = ComposeState(useLinkPreviews = useLinkPreviews) textStyle.value = smallFont chosenImage.value = null + chosenAnimImage.value = null chosenFile.value = null linkUrl.value = null prevLinkUrl.value = null @@ -376,6 +378,13 @@ fun ComposeView( mc = MsgContent.MCImage(cs.message, preview.image) } } + val chosenGifImageVal = chosenAnimImage.value + if (chosenGifImageVal != null) { + file = saveAnimImage(context, chosenGifImageVal) + if (file != null) { + mc = MsgContent.MCImage(cs.message, preview.image) + } + } } is ComposePreview.FilePreview -> { val chosenFileVal = chosenFile.value @@ -436,6 +445,7 @@ fun ComposeView( fun cancelImage() { composeState.value = composeState.value.copy(preview = ComposePreview.NoPreview) chosenImage.value = null + chosenAnimImage.value = null } fun cancelFile() { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIImageView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIImageView.kt index 139e8e31a9..4e5da7a85f 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIImageView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIImageView.kt @@ -1,4 +1,5 @@ import android.graphics.Bitmap +import android.os.Build.VERSION.SDK_INT import androidx.compose.foundation.Image import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.* @@ -13,14 +14,24 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.core.content.FileProvider +import chat.simplex.app.BuildConfig import chat.simplex.app.R import chat.simplex.app.model.CIFile import chat.simplex.app.model.CIFileStatus import chat.simplex.app.views.helpers.* +import coil.ImageLoader +import coil.compose.rememberAsyncImagePainter +import coil.decode.GifDecoder +import coil.decode.ImageDecoderDecoder +import coil.request.ImageRequest +import java.io.File @Composable fun CIImageView( @@ -88,13 +99,46 @@ fun CIImageView( ) } + @Composable + fun imageView(painter: Painter, onClick: () -> Unit) { + Image( + painter, + contentDescription = stringResource(R.string.image_descr), + // .width(1000.dp) is a hack for image to increase IntrinsicSize of FramedItemView + // if text is short and take all available width if text is long + modifier = Modifier + .width(1000.dp) + .combinedClickable( + onLongClick = { showMenu.value = true }, + onClick = onClick + ), + contentScale = ContentScale.FillWidth, + ) + } + Box(contentAlignment = Alignment.TopEnd) { val context = LocalContext.current val imageBitmap: Bitmap? = getLoadedImage(context, file) - if (imageBitmap != null) { - imageView(imageBitmap, onClick = { + val filePath = getLoadedFilePath(context, file) + if (imageBitmap != null && filePath != null) { + val uri = FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath)) + val imageLoader = ImageLoader.Builder(context) + .components { + if (SDK_INT >= 28) { + add(ImageDecoderDecoder.Factory()) + } else { + add(GifDecoder.Factory()) + } + } + .build() + val imagePainter = rememberAsyncImagePainter( + ImageRequest.Builder(context).data(data = uri).size(coil.size.Size.ORIGINAL).build(), + placeholder = BitmapPainter(imageBitmap.asImageBitmap()), // show original image while it's still loading by coil + imageLoader = imageLoader + ) + imageView(imagePainter, onClick = { if (getLoadedFilePath(context, file) != null) { - ModalManager.shared.showCustomModal { close -> ImageFullScreenView(imageBitmap, close) } + ModalManager.shared.showCustomModal { close -> ImageFullScreenView(imageBitmap, uri, close) } } }) } else { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ImageFullScreenView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ImageFullScreenView.kt index 46051fed52..13608278b9 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ImageFullScreenView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ImageFullScreenView.kt @@ -1,4 +1,6 @@ import android.graphics.Bitmap +import android.net.Uri +import android.os.Build import androidx.activity.compose.BackHandler import androidx.compose.foundation.* import androidx.compose.foundation.gestures.detectTransformGestures @@ -7,13 +9,21 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.* +import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import chat.simplex.app.R +import coil.ImageLoader +import coil.compose.rememberAsyncImagePainter +import coil.decode.GifDecoder +import coil.decode.ImageDecoderDecoder +import coil.request.ImageRequest +import coil.size.Size @Composable -fun ImageFullScreenView(imageBitmap: Bitmap, close: () -> Unit) { +fun ImageFullScreenView(imageBitmap: Bitmap, uri: Uri, close: () -> Unit) { BackHandler(onBack = close) Column( Modifier @@ -24,8 +34,23 @@ fun ImageFullScreenView(imageBitmap: Bitmap, close: () -> Unit) { var scale by remember { mutableStateOf(1f) } var translationX by remember { mutableStateOf(0f) } var translationY by remember { mutableStateOf(0f) } + // I'm making a new instance of imageLoader here because if I use one instance in multiple places + // after end of composition here a GIF from the first instance will be paused automatically which isn't what I want + val imageLoader = ImageLoader.Builder(LocalContext.current) + .components { + if (Build.VERSION.SDK_INT >= 28) { + add(ImageDecoderDecoder.Factory()) + } else { + add(GifDecoder.Factory()) + } + } + .build() Image( - imageBitmap.asImageBitmap(), + rememberAsyncImagePainter( + ImageRequest.Builder(LocalContext.current).data(data = uri).size(Size.ORIGINAL).build(), + placeholder = BitmapPainter(imageBitmap.asImageBitmap()), // show original image while it's still loading by coil + imageLoader = imageLoader + ), contentDescription = stringResource(R.string.image_descr), contentScale = ContentScale.Fit, modifier = Modifier diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Share.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Share.kt index 1045605cd1..2b46110d58 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Share.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Share.kt @@ -57,8 +57,17 @@ fun saveImage(cxt: Context, ciFile: CIFile?) { val fileName = ciFile?.fileName if (filePath != null && fileName != null) { val values = ContentValues() + val lowercaseName = fileName.lowercase() + val mimeType = when { + lowercaseName.endsWith(".png") -> "image/png" + lowercaseName.endsWith(".gif") -> "image/gif" + lowercaseName.endsWith(".webp") -> "image/webp" + lowercaseName.endsWith(".avif") -> "image/avif" + lowercaseName.endsWith(".svg") -> "image/svg+xml" + else -> "image/jpeg" + } values.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis()) - values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg") + values.put(MediaStore.Images.Media.MIME_TYPE, mimeType) values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName) values.put(MediaStore.MediaColumns.TITLE, fileName) val uri = cxt.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt index 1e635aba66..d09fcc4ce1 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt @@ -212,6 +212,7 @@ private fun spannableStringToAnnotatedString( // maximum image file size to be auto-accepted const val MAX_IMAGE_SIZE: Long = 236700 +const val MAX_IMAGE_SIZE_AUTO_RCV: Long = MAX_IMAGE_SIZE * 2 const val MAX_FILE_SIZE: Long = 8000000 fun getFilesDirectory(context: Context): String { @@ -320,6 +321,32 @@ fun saveImage(context: Context, image: Bitmap): String? { } } +fun saveAnimImage(context: Context, uri: Uri): String? { + return try { + val filename = getFileName(context, uri)?.lowercase() + var ext = when { + // remove everything but extension + filename?.contains(".") == true -> filename.replaceBeforeLast('.', "").replace(".", "") + else -> "gif" + } + // Just in case the image has a strange extension + if (ext.length < 3 || ext.length > 4) ext = "gif" + val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date()) + val fileToSave = uniqueCombine(context, "IMG_${timestamp}.$ext") + val file = File(getAppFilePath(context, fileToSave)) + val output = FileOutputStream(file) + context.contentResolver.openInputStream(uri)!!.use { input -> + output.use { output -> + input.copyTo(output) + } + } + fileToSave + } catch (e: Exception) { + Log.e(chat.simplex.app.TAG, "Util.kt saveAnimImage error: ${e.message}") + null + } +} + fun saveFileFromUri(context: Context, uri: Uri): String? { return try { val inputStream = context.contentResolver.openInputStream(uri) From 69138a24de98d184ab0b58ff0f43004c8e4503bd Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 13 Sep 2022 08:38:31 +0100 Subject: [PATCH 10/11] nix: patch out android logging from sqlcipher --- flake.nix | 12 +- .../nix/direct-sqlcipher-android-log.patch | 68 ++++ scripts/nix/direct-sqlcipher-android.patch | 324 ------------------ 3 files changed, 76 insertions(+), 328 deletions(-) create mode 100644 scripts/nix/direct-sqlcipher-android-log.patch delete mode 100644 scripts/nix/direct-sqlcipher-android.patch diff --git a/flake.nix b/flake.nix index 8e3396b296..007a9b117b 100644 --- a/flake.nix +++ b/flake.nix @@ -25,10 +25,7 @@ }; sha256map = import ./scripts/nix/sha256map.nix; modules = [{ - packages.direct-sqlcipher.patches = [ - ./scripts/nix/direct-sqlcipher-2.3.27.patch - ./scripts/nix/direct-sqlcipher-android.patch - ]; + packages.direct-sqlcipher.patches = [ ./scripts/nix/direct-sqlcipher-2.3.27.patch ]; packages.entropy.patches = [ ./scripts/nix/entropy.patch ]; } ({ pkgs,lib, ... }: lib.mkIf (pkgs.stdenv.hostPlatform.isAndroid) { @@ -125,6 +122,13 @@ pkgs' = androidPkgs; extra-modules = [{ packages.direct-sqlcipher.flags.openssl = true; + packages.direct-sqlcipher.components.library.libs = pkgs.lib.mkForce [ + (androidPkgs.openssl.override { static = true; }) + ]; + packages.direct-sqlcipher.patches = [ + ./scripts/nix/direct-sqlcipher-2.3.27.patch + ./scripts/nix/direct-sqlcipher-android-log.patch + ]; }]; }).simplex-chat.components.library.override { smallAddressSpace = true; enableShared = false; diff --git a/scripts/nix/direct-sqlcipher-android-log.patch b/scripts/nix/direct-sqlcipher-android-log.patch new file mode 100644 index 0000000000..e6df2f7942 --- /dev/null +++ b/scripts/nix/direct-sqlcipher-android-log.patch @@ -0,0 +1,68 @@ +diff --git a/cbits/sqlite3.c b/cbits/sqlite3.c +index 66bb609..00c33c1 100644 +--- a/cbits/sqlite3.c ++++ b/cbits/sqlite3.c +@@ -101739,9 +101739,9 @@ sqlite3_mutex* sqlcipher_mutex(int); + /* #include "pager.h" */ + /* #include "vdbeInt.h" */ + +-#ifdef __ANDROID__ +-#include +-#endif ++// #ifdef __ANDROID__ ++// #include ++// #endif + + /* #include */ + +@@ -104934,11 +104934,11 @@ static int sqlcipher_profile_callback(unsigned int trace, void *file, void *stmt + FILE *f = (FILE*) file; + char *fmt = "Elapsed time:%.3f ms - %s\n"; + double elapsed = (*((sqlite3_uint64*)run_time))/1000000.0; +-#ifdef __ANDROID__ +- if(f == NULL) { +- __android_log_print(ANDROID_LOG_DEBUG, "sqlcipher", fmt, elapsed, sqlite3_sql((sqlite3_stmt*)stmt)); +- } +-#endif ++// #ifdef __ANDROID__ ++// if(f == NULL) { ++// __android_log_print(ANDROID_LOG_DEBUG, "sqlcipher", fmt, elapsed, sqlite3_sql((sqlite3_stmt*)stmt)); ++// } ++// #endif + if(f) fprintf(f, fmt, elapsed, sqlite3_sql((sqlite3_stmt*)stmt)); + return SQLITE_OK; + } +@@ -104988,12 +104988,12 @@ void sqlcipher_log(unsigned int level, const char *message, ...) { + va_start(params, message); + + #ifdef CODEC_DEBUG +-#ifdef __ANDROID__ +- __android_log_vprint(ANDROID_LOG_DEBUG, "sqlcipher", message, params); +-#else ++// #ifdef __ANDROID__ ++// __android_log_vprint(ANDROID_LOG_DEBUG, "sqlcipher", message, params); ++// #else + vfprintf(stderr, message, params); + fprintf(stderr, "\n"); +-#endif ++// #endif + #endif + + if(level > sqlcipher_log_level || (sqlcipher_log_logcat == 0 && sqlcipher_log_file == NULL)) { +@@ -105026,11 +105026,11 @@ void sqlcipher_log(unsigned int level, const char *message, ...) { + fprintf((FILE*)sqlcipher_log_file, "\n"); + } + } +-#ifdef __ANDROID__ +- if(sqlcipher_log_logcat) { +- __android_log_vprint(ANDROID_LOG_DEBUG, "sqlcipher", message, params); +- } +-#endif ++// #ifdef __ANDROID__ ++// if(sqlcipher_log_logcat) { ++// __android_log_vprint(ANDROID_LOG_DEBUG, "sqlcipher", message, params); ++// } ++// #endif + end: + va_end(params); + } diff --git a/scripts/nix/direct-sqlcipher-android.patch b/scripts/nix/direct-sqlcipher-android.patch deleted file mode 100644 index 225172e4f2..0000000000 --- a/scripts/nix/direct-sqlcipher-android.patch +++ /dev/null @@ -1,324 +0,0 @@ -diff --git a/cbits/android/log.h b/cbits/android/log.h -new file mode 100644 -index 0000000..c8e715e ---- /dev/null -+++ b/cbits/android/log.h -@@ -0,0 +1,318 @@ -+/* -+ * Copyright (C) 2009 The Android Open Source Project -+ * -+ * Licensed under the Apache License, Version 2.0 (the "License"); -+ * you may not use this file except in compliance with the License. -+ * You may obtain a copy of the License at -+ * -+ * http://www.apache.org/licenses/LICENSE-2.0 -+ * -+ * Unless required by applicable law or agreed to in writing, software -+ * distributed under the License is distributed on an "AS IS" BASIS, -+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -+ * See the License for the specific language governing permissions and -+ * limitations under the License. -+ */ -+#pragma once -+/** -+ * @addtogroup Logging -+ * @{ -+ */ -+/** -+ * \file -+ * -+ * Support routines to send messages to the Android log buffer, -+ * which can later be accessed through the `logcat` utility. -+ * -+ * Each log message must have -+ * - a priority -+ * - a log tag -+ * - some text -+ * -+ * The tag normally corresponds to the component that emits the log message, -+ * and should be reasonably small. -+ * -+ * Log message text may be truncated to less than an implementation-specific -+ * limit (1023 bytes). -+ * -+ * Note that a newline character ("\n") will be appended automatically to your -+ * log message, if not already there. It is not possible to send several -+ * messages and have them appear on a single line in logcat. -+ * -+ * Please use logging in moderation: -+ * -+ * - Sending log messages eats CPU and slow down your application and the -+ * system. -+ * -+ * - The circular log buffer is pretty small, so sending many messages -+ * will hide other important log messages. -+ * -+ * - In release builds, only send log messages to account for exceptional -+ * conditions. -+ */ -+#include -+#include -+#include -+#include -+#if !defined(__BIONIC__) && !defined(__INTRODUCED_IN) -+#define __INTRODUCED_IN(x) -+#endif -+#ifdef __cplusplus -+extern "C" { -+#endif -+/** -+ * Android log priority values, in increasing order of priority. -+ */ -+typedef enum android_LogPriority { -+ /** For internal use only. */ -+ ANDROID_LOG_UNKNOWN = 0, -+ /** The default priority, for internal use only. */ -+ ANDROID_LOG_DEFAULT, /* only for SetMinPriority() */ -+ /** Verbose logging. Should typically be disabled for a release apk. */ -+ ANDROID_LOG_VERBOSE, -+ /** Debug logging. Should typically be disabled for a release apk. */ -+ ANDROID_LOG_DEBUG, -+ /** Informational logging. Should typically be disabled for a release apk. */ -+ ANDROID_LOG_INFO, -+ /** Warning logging. For use with recoverable failures. */ -+ ANDROID_LOG_WARN, -+ /** Error logging. For use with unrecoverable failures. */ -+ ANDROID_LOG_ERROR, -+ /** Fatal logging. For use when aborting. */ -+ ANDROID_LOG_FATAL, -+ /** For internal use only. */ -+ ANDROID_LOG_SILENT, /* only for SetMinPriority(); must be last */ -+} android_LogPriority; -+/** -+ * Writes the constant string `text` to the log, with priority `prio` and tag -+ * `tag`. -+ */ -+int __android_log_write(int prio, const char* tag, const char* text); -+/** -+ * Writes a formatted string to the log, with priority `prio` and tag `tag`. -+ * The details of formatting are the same as for -+ * [printf(3)](http://man7.org/linux/man-pages/man3/printf.3.html). -+ */ -+int __android_log_print(int prio, const char* tag, const char* fmt, ...) -+ __attribute__((__format__(printf, 3, 4))); -+/** -+ * Equivalent to `__android_log_print`, but taking a `va_list`. -+ * (If `__android_log_print` is like `printf`, this is like `vprintf`.) -+ */ -+int __android_log_vprint(int prio, const char* tag, const char* fmt, va_list ap) -+ __attribute__((__format__(printf, 3, 0))); -+/** -+ * Writes an assertion failure to the log (as `ANDROID_LOG_FATAL`) and to -+ * stderr, before calling -+ * [abort(3)](http://man7.org/linux/man-pages/man3/abort.3.html). -+ * -+ * If `fmt` is non-null, `cond` is unused. If `fmt` is null, the string -+ * `Assertion failed: %s` is used with `cond` as the string argument. -+ * If both `fmt` and `cond` are null, a default string is provided. -+ * -+ * Most callers should use -+ * [assert(3)](http://man7.org/linux/man-pages/man3/assert.3.html) from -+ * `<assert.h>` instead, or the `__assert` and `__assert2` functions -+ * provided by bionic if more control is needed. They support automatically -+ * including the source filename and line number more conveniently than this -+ * function. -+ */ -+void __android_log_assert(const char* cond, const char* tag, const char* fmt, ...) -+ __attribute__((__noreturn__)) __attribute__((__format__(printf, 3, 4))); -+/** -+ * Identifies a specific log buffer for __android_log_buf_write() -+ * and __android_log_buf_print(). -+ */ -+typedef enum log_id { -+ LOG_ID_MIN = 0, -+ /** The main log buffer. This is the only log buffer available to apps. */ -+ LOG_ID_MAIN = 0, -+ /** The radio log buffer. */ -+ LOG_ID_RADIO = 1, -+ /** The event log buffer. */ -+ LOG_ID_EVENTS = 2, -+ /** The system log buffer. */ -+ LOG_ID_SYSTEM = 3, -+ /** The crash log buffer. */ -+ LOG_ID_CRASH = 4, -+ /** The statistics log buffer. */ -+ LOG_ID_STATS = 5, -+ /** The security log buffer. */ -+ LOG_ID_SECURITY = 6, -+ /** The kernel log buffer. */ -+ LOG_ID_KERNEL = 7, -+ LOG_ID_MAX, -+ /** Let the logging function choose the best log target. */ -+ LOG_ID_DEFAULT = 0x7FFFFFFF -+} log_id_t; -+/** -+ * Writes the constant string `text` to the log buffer `id`, -+ * with priority `prio` and tag `tag`. -+ * -+ * Apps should use __android_log_write() instead. -+ */ -+int __android_log_buf_write(int bufID, int prio, const char* tag, const char* text); -+/** -+ * Writes a formatted string to log buffer `id`, -+ * with priority `prio` and tag `tag`. -+ * The details of formatting are the same as for -+ * [printf(3)](http://man7.org/linux/man-pages/man3/printf.3.html). -+ * -+ * Apps should use __android_log_print() instead. -+ */ -+int __android_log_buf_print(int bufID, int prio, const char* tag, const char* fmt, ...) -+ __attribute__((__format__(printf, 4, 5))); -+/** -+ * Logger data struct used for writing log messages to liblog via __android_log_write_logger_data() -+ * and sending log messages to user defined loggers specified in __android_log_set_logger(). -+ */ -+struct __android_log_message { -+ size_t -+ struct_size; /** Must be set to sizeof(__android_log_message) and is used for versioning. */ -+ int32_t buffer_id; /** {@link log_id_t} values. */ -+ int32_t priority; /** {@link android_LogPriority} values. */ -+ const char* tag; /** The tag for the log message. */ -+ const char* file; /** Optional file name, may be set to nullptr. */ -+ uint32_t line; /** Optional line number, ignore if file is nullptr. */ -+ const char* message; /** The log message itself. */ -+}; -+/** -+ * Prototype for the 'logger' function that is called for every log message. -+ */ -+typedef void (*__android_logger_function)(const struct __android_log_message* log_message); -+/** -+ * Prototype for the 'abort' function that is called when liblog will abort due to -+ * __android_log_assert() failures. -+ */ -+typedef void (*__android_aborter_function)(const char* abort_message); -+#if !defined(__ANDROID__) || __ANDROID_API__ >= 30 -+/** -+ * Writes the log message specified by log_message. log_message includes additional file name and -+ * line number information that a logger may use. log_message is versioned for backwards -+ * compatibility. -+ * This assumes that loggability has already been checked through __android_log_is_loggable(). -+ * Higher level logging libraries, such as libbase, first check loggability, then format their -+ * buffers, then pass the message to liblog via this function, and therefore we do not want to -+ * duplicate the loggability check here. -+ * -+ * @param log_message the log message itself, see {@link __android_log_message}. -+ * -+ * Available since API level 30. -+ */ -+void __android_log_write_log_message(struct __android_log_message* log_message) __INTRODUCED_IN(30); -+/** -+ * Sets a user defined logger function. All log messages sent to liblog will be set to the -+ * function pointer specified by logger for processing. It is not expected that log messages are -+ * already terminated with a new line. This function should add new lines if required for line -+ * separation. -+ * -+ * @param logger the new function that will handle log messages. -+ * -+ * Available since API level 30. -+ */ -+void __android_log_set_logger(__android_logger_function logger) __INTRODUCED_IN(30); -+/** -+ * Writes the log message to logd. This is an __android_logger_function and can be provided to -+ * __android_log_set_logger(). It is the default logger when running liblog on a device. -+ * -+ * @param log_message the log message to write, see {@link __android_log_message}. -+ * -+ * Available since API level 30. -+ */ -+void __android_log_logd_logger(const struct __android_log_message* log_message) __INTRODUCED_IN(30); -+/** -+ * Writes the log message to stderr. This is an __android_logger_function and can be provided to -+ * __android_log_set_logger(). It is the default logger when running liblog on host. -+ * -+ * @param log_message the log message to write, see {@link __android_log_message}. -+ * -+ * Available since API level 30. -+ */ -+void __android_log_stderr_logger(const struct __android_log_message* log_message) -+ __INTRODUCED_IN(30); -+/** -+ * Sets a user defined aborter function that is called for __android_log_assert() failures. This -+ * user defined aborter function is highly recommended to abort and be noreturn, but is not strictly -+ * required to. -+ * -+ * @param aborter the new aborter function, see {@link __android_aborter_function}. -+ * -+ * Available since API level 30. -+ */ -+void __android_log_set_aborter(__android_aborter_function aborter) __INTRODUCED_IN(30); -+/** -+ * Calls the stored aborter function. This allows for other logging libraries to use the same -+ * aborter function by calling this function in liblog. -+ * -+ * @param abort_message an additional message supplied when aborting, for example this is used to -+ * call android_set_abort_message() in __android_log_default_aborter(). -+ * -+ * Available since API level 30. -+ */ -+void __android_log_call_aborter(const char* abort_message) __INTRODUCED_IN(30); -+/** -+ * Sets android_set_abort_message() on device then aborts(). This is the default aborter. -+ * -+ * @param abort_message an additional message supplied when aborting. This functions calls -+ * android_set_abort_message() with its contents. -+ * -+ * Available since API level 30. -+ */ -+void __android_log_default_aborter(const char* abort_message) __attribute__((noreturn)) -+__INTRODUCED_IN(30); -+/** -+ * Use the per-tag properties "log.tag." along with the minimum priority from -+ * __android_log_set_minimum_priority() to determine if a log message with a given prio and tag will -+ * be printed. A non-zero result indicates yes, zero indicates false. -+ * -+ * If both a priority for a tag and a minimum priority are set by -+ * __android_log_set_minimum_priority(), then the lowest of the two values are to determine the -+ * minimum priority needed to log. If only one is set, then that value is used to determine the -+ * minimum priority needed. If none are set, then default_priority is used. -+ * -+ * @param prio the priority to test, takes {@link android_LogPriority} values. -+ * @param tag the tag to test. -+ * @param len the length of the tag. -+ * @param default_prio the default priority to use if no properties or minimum priority are set. -+ * @return an integer where 1 indicates that the message is loggable and 0 indicates that it is not. -+ * -+ * Available since API level 30. -+ */ -+int __android_log_is_loggable(int prio, const char* tag, int default_prio) __INTRODUCED_IN(30); -+int __android_log_is_loggable_len(int prio, const char* tag, size_t len, int default_prio) -+ __INTRODUCED_IN(30); -+/** -+ * Sets the minimum priority that will be logged for this process. -+ * -+ * @param priority the new minimum priority to set, takes @{link android_LogPriority} values. -+ * @return the previous set minimum priority as @{link android_LogPriority} values, or -+ * ANDROID_LOG_DEFAULT if none was set. -+ * -+ * Available since API level 30. -+ */ -+int32_t __android_log_set_minimum_priority(int32_t priority) __INTRODUCED_IN(30); -+/** -+ * Gets the minimum priority that will be logged for this process. If none has been set by a -+ * previous __android_log_set_minimum_priority() call, this returns ANDROID_LOG_DEFAULT. -+ * -+ * @return the current minimum priority as @{link android_LogPriority} values, or -+ * ANDROID_LOG_DEFAULT if none is set. -+ * -+ * Available since API level 30. -+ */ -+int32_t __android_log_get_minimum_priority(void) __INTRODUCED_IN(30); -+/** -+ * Sets the default tag if no tag is provided when writing a log message. Defaults to -+ * getprogname(). This truncates tag to the maximum log message size, though appropriate tags -+ * should be much smaller. -+ * -+ * @param tag the new log tag. -+ * -+ * Available since API level 30. -+ */ -+void __android_log_set_default_tag(const char* tag) __INTRODUCED_IN(30); -+#endif -+#ifdef __cplusplus -+} -+#endif -+/** @} */ From 7f9e68c58d889c7fed52a11329eae55a17e9feaa Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 13 Sep 2022 12:05:18 +0100 Subject: [PATCH 11/11] remove duplicate patch --- flake.nix | 1 - 1 file changed, 1 deletion(-) diff --git a/flake.nix b/flake.nix index 007a9b117b..69ecf3e507 100644 --- a/flake.nix +++ b/flake.nix @@ -126,7 +126,6 @@ (androidPkgs.openssl.override { static = true; }) ]; packages.direct-sqlcipher.patches = [ - ./scripts/nix/direct-sqlcipher-2.3.27.patch ./scripts/nix/direct-sqlcipher-android-log.patch ]; }];