diff --git a/CMakeLists.txt b/CMakeLists.txt index 004e509..c8b1f9e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,9 +5,11 @@ include(GNUInstallDirs) set(TERMUX_PREFIX ${CMAKE_INSTALL_PREFIX}) add_library(termux-api SHARED termux-api.c) +add_library(termux-api-static STATIC termux-api.c) +set_target_properties(termux-api-static PROPERTIES OUTPUT_NAME termux-api) add_executable(termux-api-broadcast termux-api-broadcast.c) -target_link_libraries(termux-api-broadcast termux-api) +target_link_libraries(termux-api-broadcast termux-api-static) # TODO: get list through regex or similar set(script_files @@ -35,8 +37,18 @@ set(script_files scripts/termux-microphone-record scripts/termux-nfc scripts/termux-notification + scripts/termux-notification-channel scripts/termux-notification-list scripts/termux-notification-remove + scripts/termux-saf-create + scripts/termux-saf-dirs + scripts/termux-saf-ls + scripts/termux-saf-managedir + scripts/termux-saf-mkdir + scripts/termux-saf-read + scripts/termux-saf-rm + scripts/termux-saf-stat + scripts/termux-saf-write scripts/termux-sensor scripts/termux-share scripts/termux-sms-inbox @@ -91,7 +103,9 @@ INSTALL(CODE "execute_process( \ ) install( - FILES ${CMAKE_BINARY_DIR}/libtermux-api.so + FILES + ${CMAKE_BINARY_DIR}/libtermux-api.so + ${CMAKE_BINARY_DIR}/libtermux-api.a TYPE LIB ) diff --git a/scripts/termux-notification-channel.in b/scripts/termux-notification-channel.in new file mode 100644 index 0000000..1fa91d3 --- /dev/null +++ b/scripts/termux-notification-channel.in @@ -0,0 +1,37 @@ +#!@TERMUX_PREFIX@/bin/bash +set -e -u + +SCRIPTNAME=termux-notification-channel +show_usage () { + echo "Usage: $SCRIPTNAME -d channel-id" + echo " $SCRIPTNAME channel-id channel-name" + echo "Create or delete a notification channel." + echo "Only usable on Android 8.0 and higher." + echo "Use -d to delete a channel." + echo "Creating a channel requires a channel id and a channel name." + echo "The name will be visible in the options, the id is used to send notifications on that specific channel." + echo "Creating a channel with the same id again will change the name." + echo "Creating a channel with the same id as a deleted channel will restore the user settings of the deleted channel." + echo "Use termux-notification --channel channel-id to send a notification on a custom channel." + exit 0 +} + +ARGS="" + +if [ "$1" = "-d" ]; then + shift + if [ $# == 1 ]; then + ARGS="--ez delete true --es id $1" + else + show_usage + fi +else + if [ $# == 2 ]; then + ARGS="--es id $1 --es name $2" + else + show_usage + fi +fi + + +@TERMUX_PREFIX@/libexec/termux-api NotificationChannel $ARGS diff --git a/scripts/termux-notification.in b/scripts/termux-notification.in index d3966a8..61ee10b 100644 --- a/scripts/termux-notification.in +++ b/scripts/termux-notification.in @@ -17,6 +17,10 @@ show_usage () { echo " -c/--content content content to show in the notification. Will take" echo " precedence over stdin. If content is not passed as" echo " an argument or with stdin, then there will be a 3s delay." + echo " --channel channel-id Specifies the notification channel id this notification should be send on." + echo " On Android versions lower than 8.0 this is a no-op." + echo " Create custom channels with termux-notification-channel." + echo " If the channel id is invalid, the notification will not be send." echo " --group group notification group (notifications with the same" echo " group are shown together)" echo " -h/--help show this help" @@ -88,6 +92,7 @@ OPT_BUTTON3_ACTION="" OPT_BUTTON3_TEXT="" OPT_CONTENT="" OPT_CONTENT_PASSED="" +OPT_CHANNEL="" OPT_GROUP="" OPT_ID="" OPT_ICON="" @@ -114,7 +119,7 @@ TEMP=`getopt \ button1:,button1-action:,\ button2:,button2-action:,\ button3:,button3-action:,\ -content:,group:,help,help-actions,\ +content:,channel:,group:,help,help-actions,\ id:,icon:,image-path:,\ led-color:,led-on:,led-off:,\ media-previous:,media-next:,media-play:,media-pause:,\ @@ -136,6 +141,7 @@ while true; do --button3) OPT_BUTTON3_TEXT="$2"; shift 2;; --button3-action) OPT_BUTTON3_ACTION="$2"; shift 2;; -c | --content) OPT_CONTENT_PASSED=1; OPT_CONTENT="$2"; shift 2;; + --channel) OPT_CHANNEL="$2"; shift 2;; --group) OPT_GROUP="$2"; shift 2;; -h | --help) show_usage;; --help-actions) show_help_actions; exit 0;; @@ -177,6 +183,7 @@ if [ -n "$OPT_BUTTON2_ACTION" ]; then set -- "$@" --es button_action_2 "$OPT_BUT if [ -n "$OPT_BUTTON2_TEXT" ]; then set -- "$@" --es button_text_2 "$OPT_BUTTON2_TEXT"; fi if [ -n "$OPT_BUTTON3_ACTION" ]; then set -- "$@" --es button_action_3 "$OPT_BUTTON3_ACTION"; fi if [ -n "$OPT_BUTTON3_TEXT" ]; then set -- "$@" --es button_text_3 "$OPT_BUTTON3_TEXT"; fi +if [ -n "$OPT_CHANNEL" ]; then set -- "$@" --es channel "$OPT_CHANNEL"; fi if [ -n "$OPT_GROUP" ]; then set -- "$@" --es group "$OPT_GROUP"; fi if [ -n "$OPT_ID" ]; then set -- "$@" --es id "$OPT_ID"; fi if [ -n "$OPT_ICON" ]; then set -- "$@" --es icon "$OPT_ICON"; fi diff --git a/scripts/termux-saf-create.in b/scripts/termux-saf-create.in new file mode 100644 index 0000000..8b50e16 --- /dev/null +++ b/scripts/termux-saf-create.in @@ -0,0 +1,33 @@ +#!@TERMUX_PREFIX@/bin/sh +set -e -u + +SCRIPTNAME=termux-saf-create +show_usage () { + echo "Usage: $SCRIPTNAME [-t mime-type] folder-uri name" + echo "Creates a file in a folder managed by Termux:API." + echo "Returns the URI you can use to read and write the file with termux-saf-read and termux-saf-write." + echo "You can specify the mime type explicitly or let it be guessed based on the file extension." + echo "As the folder URI you can use the URI of a directory listed by termux-saf-dirs or by termux-saf-ls." + echo "Android doesn't allow creating files with the same name, so the name may be changed if a file or folder with that name already exists." + echo "You can use termux-saf-stat with the returned URI to get the name it was really given." + echo " -h show this help" + echo " -t specify the mime type of the file. The mime type is required for other apps to recognize the content as e.g. a video. If not specified, 'application/octet-stream' is assumed, that means raw binary data." + exit 0 +} + +mime='' + +while getopts :ht: option +do + case "$option" in + h) show_usage;; + t) mime="$OPTARG";; + ?) echo "$SCRIPTNAME: illegal option -$OPTARG"; exit 1; + esac +done +shift $((OPTIND-1)) + +if [ $# != 2 ]; then echo "$SCRIPTNAME: Invalid argument count"; exit 1; fi + +@TERMUX_PREFIX@/libexec/termux-api SAF --es safmethod createDocument --es treeuri "$1" --es filename "$2" --es mimetype "$mime" + diff --git a/scripts/termux-saf-dirs.in b/scripts/termux-saf-dirs.in new file mode 100644 index 0000000..e62e4df --- /dev/null +++ b/scripts/termux-saf-dirs.in @@ -0,0 +1,27 @@ +#!@TERMUX_PREFIX@/bin/sh +set -e -u + +SCRIPTNAME=termux-saf-dirs +show_usage () { + echo "Usage: $SCRIPTNAME" + echo "Lists all directories you gave Termux:API in the same format as termux-saf-ls." + echo "That meas this lists the 'directory' that contains all directories you can access with the other termux-saf-* commands." + echo "The URIs can be used with the other termux-saf-* commands to create files and folders and list the directory contents." + echo "See termux-saf-managedir to give Termux:API access to a folder." + echo " -h show this help" + exit 0 +} + +while getopts :h option +do + case "$option" in + h) show_usage;; + ?) echo "$SCRIPTNAME: illegal option -$OPTARG"; exit 1; + esac +done +shift $((OPTIND-1)) + +if [ $# != 0 ]; then echo "$SCRIPTNAME: too many arguments"; exit 1; fi + +@TERMUX_PREFIX@/libexec/termux-api SAF --es safmethod getManagedDocumentTrees + diff --git a/scripts/termux-saf-ls.in b/scripts/termux-saf-ls.in new file mode 100644 index 0000000..7aeb807 --- /dev/null +++ b/scripts/termux-saf-ls.in @@ -0,0 +1,36 @@ +#!@TERMUX_PREFIX@/bin/sh +set -e -u + +SCRIPTNAME=termux-saf-ls +show_usage () { + echo "Usage: $SCRIPTNAME folder-uri" + echo "Lists the files and folders in a folder identified by a URI." + echo "You can get a folder URI by:" + echo "- Listing folders with termux-saf-ls" + echo "- Giving Termux:API access to folders with termux-saf-managedir" + echo "- Listing the folders you gave Termux:API access to with termux-saf-dirs" + echo "- Creating a folder with termux-saf-mkdir" + echo "The list is returned as a JSON array with one JSON object per entry." + echo "The objects have the keys:" + echo "- 'name' for the human-readable name" + echo "- 'uri' for URI you can use with the other termux-saf-* commands" + echo "- 'type' for the mime type" + echo "- 'length' for size in bytes if it's a file" + echo "You can recognize folders by the special mime type 'vnd.android.document/directory'." + echo " -h show this help" + exit 0 +} + +while getopts :h option +do + case "$option" in + h) show_usage;; + ?) echo "$SCRIPTNAME: illegal option -$OPTARG"; exit 1; + esac +done +shift $((OPTIND-1)) + +if [ $# != 1 ]; then echo "$SCRIPTNAME: Invalid argument count"; exit 1; fi + +@TERMUX_PREFIX@/libexec/termux-api SAF --es safmethod listDirectory --es treeuri "$1" + diff --git a/scripts/termux-saf-managedir.in b/scripts/termux-saf-managedir.in new file mode 100644 index 0000000..1567033 --- /dev/null +++ b/scripts/termux-saf-managedir.in @@ -0,0 +1,28 @@ +#!@TERMUX_PREFIX@/bin/sh +set -e -u + +SCRIPTNAME=termux-saf-managedir +show_usage () { + echo "Usage: $SCRIPTNAME" + echo "Opens the system file explorer and lets you specify a folder Termux:API get access to." + echo "You can then use the termux-saf-* utilities to manage the contents in that folder." + echo "Returns a URI identifying the directory, this URI can be used with the other termux-saf-* commands to create files and folders and list the directory contents." + echo "If you close the file manager instead, an empty string will be returned." + echo "You can use termux-saf-dirs to list out all directories you gave Termux:API access to like this." + echo " -h show this help" + exit 0 +} + +while getopts :h option +do + case "$option" in + h) show_usage;; + ?) echo "$SCRIPTNAME: illegal option -$OPTARG"; exit 1; + esac +done +shift $((OPTIND-1)) + +if [ $# != 0 ]; then echo "$SCRIPTNAME: too many arguments"; exit 1; fi + +@TERMUX_PREFIX@/libexec/termux-api SAF --es safmethod manageDocumentTree + diff --git a/scripts/termux-saf-mkdir.in b/scripts/termux-saf-mkdir.in new file mode 100644 index 0000000..2dbc28d --- /dev/null +++ b/scripts/termux-saf-mkdir.in @@ -0,0 +1,25 @@ +#!@TERMUX_PREFIX@/bin/sh +set -e -u + +SCRIPTNAME=termux-saf-mkdir +show_usage () { + echo "Usage: $SCRIPTNAME parent-uri name" + echo "Creates a directory via SAF." + echo "This behaves like termux-saf-create, just for directories." + echo " -h show this help" + exit 0 +} + +while getopts :h option +do + case "$option" in + h) show_usage;; + ?) echo "$SCRIPTNAME: illegal option -$OPTARG"; exit 1; + esac +done +shift $((OPTIND-1)) + +if [ $# != 2 ]; then echo "$SCRIPTNAME: Invalid argument count"; exit 1; fi + +@TERMUX_PREFIX@/libexec/termux-api SAF --es safmethod createDocument --es treeuri "$1" --es filename "$2" --es mimetype 'vnd.android.document/directory' + diff --git a/scripts/termux-saf-read.in b/scripts/termux-saf-read.in new file mode 100644 index 0000000..89c195e --- /dev/null +++ b/scripts/termux-saf-read.in @@ -0,0 +1,26 @@ +#!@TERMUX_PREFIX@/bin/sh +set -e -u + +SCRIPTNAME=termux-saf-read +show_usage () { + echo "Usage: $SCRIPTNAME uri" + echo "Reads from a file identified by a URI and writes the data to sstandard output." + echo "You can use pipes to process the data or redirect it into a file to make a local copy." + echo "See termux-saf-list to get the URIs to files." + echo " -h show this help" + exit 0 +} + +while getopts :h option +do + case "$option" in + h) show_usage;; + ?) echo "$SCRIPTNAME: illegal option -$OPTARG"; exit 1; + esac +done +shift $((OPTIND-1)) + +if [ $# != 1 ]; then echo "$SCRIPTNAME: Invalid argument count"; exit 1; fi + +@TERMUX_PREFIX@/libexec/termux-api SAF --es safmethod readDocument --es uri "$1" + diff --git a/scripts/termux-saf-rm.in b/scripts/termux-saf-rm.in new file mode 100644 index 0000000..0d41316 --- /dev/null +++ b/scripts/termux-saf-rm.in @@ -0,0 +1,25 @@ +#!@TERMUX_PREFIX@/bin/sh +set -e -u + +SCRIPTNAME=termux-saf-rm +show_usage () { + echo "Usage: $SCRIPTNAME uri" + echo "Removes the file or folder at the given URI. See other termux-saf-* commands for more info." + echo "Returns 0 on success, 1 if the file or folder couldn't be deleted and 2 if another error occurred." + echo " -h show this help" + exit 0 +} + +while getopts :h option +do + case "$option" in + h) show_usage;; + ?) echo "$SCRIPTNAME: illegal option -$OPTARG"; exit 1; + esac +done +shift $((OPTIND-1)) + +if [ $# != 1 ]; then echo "$SCRIPTNAME: Invalid argument count"; exit 1; fi + +exit "$(@TERMUX_PREFIX@/libexec/termux-api SAF --es safmethod removeDocument --es uri "$1")" + diff --git a/scripts/termux-saf-stat.in b/scripts/termux-saf-stat.in new file mode 100644 index 0000000..8a49f4e --- /dev/null +++ b/scripts/termux-saf-stat.in @@ -0,0 +1,25 @@ +#!@TERMUX_PREFIX@/bin/sh +set -e -u + +SCRIPTNAME=termux-saf-stat +show_usage () { + echo "Usage: $SCRIPTNAME uri" + echo "Returns the file or folder info as a JSON object." + echo "The format is the same as an entry from termux-saf-ls." + echo " -h show this help" + exit 0 +} + +while getopts :h option +do + case "$option" in + h) show_usage;; + ?) echo "$SCRIPTNAME: illegal option -$OPTARG"; exit 1; + esac +done +shift $((OPTIND-1)) + +if [ $# != 1 ]; then echo "$SCRIPTNAME: Invalid argument count"; exit 1; fi + +@TERMUX_PREFIX@/libexec/termux-api SAF --es safmethod statURI --es uri "$1" + diff --git a/scripts/termux-saf-write.in b/scripts/termux-saf-write.in new file mode 100644 index 0000000..5109bde --- /dev/null +++ b/scripts/termux-saf-write.in @@ -0,0 +1,24 @@ +#!@TERMUX_PREFIX@/bin/sh +set -e -u + +SCRIPTNAME=termux-saf-write +show_usage () { + echo "Usage: $SCRIPTNAME uri" + echo "Writes the standard input into an existing file identified by a URI. The previous contents are erased. To create a new file, use termux-saf-create." + echo " -h show this help" + exit 0 +} + +while getopts :h option +do + case "$option" in + h) show_usage;; + ?) echo "$SCRIPTNAME: illegal option -$OPTARG"; exit 1; + esac +done +shift $((OPTIND-1)) + +if [ $# != 1 ]; then echo "$SCRIPTNAME: Invalid argument count"; exit 1; fi + +@TERMUX_PREFIX@/libexec/termux-api SAF --es safmethod writeDocument --es uri "$1" + diff --git a/scripts/termux-sms-list.in b/scripts/termux-sms-list.in index b700b0c..dbeab67 100644 --- a/scripts/termux-sms-list.in +++ b/scripts/termux-sms-list.in @@ -13,7 +13,7 @@ SUPPORTED_TYPES="all|inbox|sent|draft|outbox" show_usage () { echo "Usage: $SCRIPTNAME [-d] [-l limit] [-n] [-o offset] [-t type] [-c] [-f number]" echo "List SMS messages." - echo " -l limit offset in sms list (default: $PARAM_LIMIT)" + echo " -l limit limit in retrieved sms list (default: $PARAM_LIMIT)" echo " -o offset offset in sms list (default: $PARAM_OFFSET)" echo " -t type the type of messages to list (default: $PARAM_TYPE):" echo " $SUPPORTED_TYPES" diff --git a/termux-api.c b/termux-api.c index d6d8b5b..68dd822 100644 --- a/termux-api.c +++ b/termux-api.c @@ -16,8 +16,14 @@ #include #include +#ifdef __ANDROID__ +#include +#endif + #include "termux-api.h" +#define TERMUX_API_PACKAGE_VERSION "0.58.0" + #ifndef PREFIX # define PREFIX "/data/data/com.termux/files/usr" #endif @@ -44,8 +50,21 @@ _Noreturn void contact_plugin(int argc, char** argv, }; sigaction(SIGPIPE, &sigpipe_action, NULL); - // try to connect over the listen socket first - int listenfd = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0); + // Try to connect over the listen socket first if running on Android `< 14`. + // On Android `>= 14`, if termux-api app process was started previously + // and it started the socket server, but later Android froze the + // process, the socket will still be connectable, but no response + // will be received until the app process is unfrozen agin and + // `read()` call below will hang indefinitely until that happens, + // so use legacy `am broadcast` command, which will also unfreeze + // the app process to deliver the intent. + // - https://github.com/termux/termux-api/issues/638#issuecomment-1813233924 + int listenfd = -1; + #ifdef __ANDROID__ + if (android_get_device_api_level() < 34) { + listenfd = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0); + } + #endif if (listenfd != -1) { struct sockaddr_un listen_addr = { .sun_family = AF_UNIX }; memcpy(listen_addr.sun_path+1, LISTEN_SOCKET_ADDRESS, strlen(LISTEN_SOCKET_ADDRESS)); @@ -59,7 +78,10 @@ _Noreturn void contact_plugin(int argc, char** argv, const char outsock_str[] = "--es socket_output \""; const char method_str[] = "--es api_method \""; - int len = sizeof(insock_str)-1+strlen(output_address_string)+2+sizeof(outsock_str)-1+strlen(input_address_string)+2+sizeof(method_str)-1+strlen(argv[1])+2; + int len = 0; + len += sizeof(insock_str)-1 + strlen(output_address_string)+2; + len += sizeof(outsock_str)-1 + strlen(input_address_string)+2; + len += sizeof(method_str)-1 + strlen(argv[1])+2; for (int i = 2; i 0) { - // if a single null byte is received as the first message, the call was successfull + // if a single null byte is received as the first message, the call was successful if (ret == 1 && readbuffer[0] == 0 && first) { err = false; break; @@ -245,7 +270,7 @@ _Noreturn void exec_am_broadcast(int argc, char** argv, memcpy(child_argv + extra_args, argv + 2, (argc-1) * sizeof(char*)); // End with NULL: - child_argv[argc + extra_args] = NULL; + child_argv[argc + extra_args - 1] = NULL; // Use an a executable taking care of PATH and LD_LIBRARY_PATH: execv(PREFIX "/bin/am", child_argv); @@ -341,6 +366,13 @@ int transmit_socket_to_stdout(int input_socket_fd) { } int run_api_command(int argc, char **argv) { + // If only `--version` argument is passed + if (argc == 2 && strcmp(argv[1], "--version") == 0) { + fprintf(stdout, "%s\n", TERMUX_API_PACKAGE_VERSION); + fflush(stdout); + exit(0); + } + // Do not transform children into zombies when they terminate: struct sigaction sigchld_action = { .sa_handler = SIG_DFL, diff --git a/termux-api.h b/termux-api.h index 9c07b3e..c3a8edf 100644 --- a/termux-api.h +++ b/termux-api.h @@ -1,5 +1,12 @@ +#ifndef TERMUX_API_H +#define TERMUX_API_H + #include +#if defined(__cplusplus) +extern "C" { +#endif + _Noreturn void exec_am_broadcast(int, char**, char*, char*); _Noreturn void contact_plugin(int, char**, char*, char*); _Noreturn void exec_callback(int); @@ -7,3 +14,9 @@ void generate_uuid(char*); void* transmit_stdin_to_socket(void*); int transmit_socket_to_stdout(int); int run_api_command(int, char**); + +#if defined(__cplusplus) +} +#endif + +#endif /* TERMUX_API_H */