Skip to content

Commit

Permalink
TgBot++: clientapp: Android: Update code
Browse files Browse the repository at this point in the history
- Now the fab is used to ask storage permissions
- Current setting is shown on every fragment
- Upload file entry is added
- Make a unified interface for storage permission request
- Use nonblocking sockets for the ability to time out
- Use new logging lib
- Add unified viewmodel for single livedata
  • Loading branch information
Royna2544 committed Jun 12, 2024
1 parent bc327bd commit cf30ccb
Show file tree
Hide file tree
Showing 49 changed files with 991 additions and 252 deletions.
4 changes: 2 additions & 2 deletions clientapp/android/.idea/deploymentTargetSelector.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 12 additions & 8 deletions clientapp/android/app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import com.android.ide.common.gradle.RELEASE

plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.jetbrains.kotlin.android)
Expand All @@ -15,16 +17,16 @@ android {
versionName = "1.0"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
cppFlags += ""
}
}
}

buildTypes {
release {
isMinifyEnabled = false
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
debug {
applicationIdSuffix = ".dev"
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
Expand Down Expand Up @@ -52,13 +54,15 @@ dependencies {
implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation(libs.androidx.activity)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.lifecycle.livedata.ktx)
implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.androidx.navigation.fragment.ktx)
implementation(libs.androidx.navigation.ui.ktx)
implementation(libs.androidx.preference.ktx)
implementation(libs.androidx.preference)
implementation(libs.androidx.fragment.ktx)
implementation(libs.oshai.kotlin.logging.jvm)
runtimeOnly(libs.slf4j.simple)

testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
Expand Down
2 changes: 1 addition & 1 deletion clientapp/android/app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
Expand Down
2 changes: 2 additions & 0 deletions clientapp/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />

<application
Expand Down
132 changes: 103 additions & 29 deletions clientapp/android/app/src/main/cpp/tgbotclient.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include <jni.h>
#include <unistd.h>
#include <zlib.h>
#include <sys/poll.h>

#include <cstring>
#include <string>
Expand All @@ -26,8 +27,17 @@ using namespace TgBotSocket::callback;
struct SocketConfig {
std::string address;
enum { USE_IPV4, USE_IPV6 } mode;
int port;

int timeout_s = 5;
};

#define SOCKNATIVE_JAVACLS "com/royna/tgbotclient/SocketCommandNative"
#define SOCKNATIVE_CMDCB_JAVACLS SOCKNATIVE_JAVACLS "$ICommandCallback"
#define SOCKNATIVE_DSTINFO_JAVACLS SOCKNATIVE_JAVACLS "$DestinationInfo"
#define SOCKNATIVE_DSTTYPE_JAVACLS SOCKNATIVE_JAVACLS "$DestinationType"
#define STRING_JAVACLS "java/lang/String"

class TgBotSocketNative {
public:
void sendContext(Packet &packet, JNIEnv *env, jobject callbackObj) const {
Expand All @@ -50,6 +60,7 @@ class TgBotSocketNative {
void setSocketConfig(SocketConfig config_in) {
config = std::move(config_in);
}
SocketConfig getSocketConfig() { return config; }

static std::shared_ptr<TgBotSocketNative> getInstance() {
static auto instance =
Expand All @@ -59,7 +70,7 @@ class TgBotSocketNative {

static void callSuccess(JNIEnv *env, jobject callbackObj,
jobject resultObj) {
jclass callbackClass = env->FindClass(kCallbackCls.data());
jclass callbackClass = env->FindClass(SOCKNATIVE_CMDCB_JAVACLS);
jmethodID success = env->GetMethodID(callbackClass, "onSuccess",
"(Ljava/lang/Object;)V");
env->CallVoidMethod(callbackObj, success, resultObj);
Expand All @@ -68,19 +79,16 @@ class TgBotSocketNative {

static void callFailed(JNIEnv *env, jobject callbackObj,
std::string message) {
jclass callbackClass = env->FindClass(kCallbackCls.data());
jclass callbackClass = env->FindClass(SOCKNATIVE_CMDCB_JAVACLS);
jmethodID success =
env->GetMethodID(callbackClass, "onError", "(Ljava/lang/String;)V");
env->GetMethodID(callbackClass, "onError", "(L" STRING_JAVACLS ";)V");
env->CallVoidMethod(callbackObj, success,
Convert<jstring>(env, message));
ABSL_LOG(INFO) << "Called onFailed callback";
}

private:
constexpr static int kSocketPort = 50000;
constexpr static int kRecvFlags = MSG_WAITALL | MSG_NOSIGNAL;
static constexpr std::string_view kCallbackCls =
"com/royna/tgbotclient/SocketCommandNative$ICommandCallback";
SocketConfig config;

TgBotSocketNative() = default;
Expand All @@ -102,7 +110,7 @@ class TgBotSocketNative {
int ret{};

ABSL_LOG(INFO) << "Prepare to send CommandContext";
sockfd = socket(af, SOCK_STREAM, 0);
sockfd = socket(af, SOCK_STREAM | SOCK_NONBLOCK, 0);
if (sockfd < 0) {
_callFailed(env, callbackObj, "Failed to create socket");
return;
Expand All @@ -111,7 +119,7 @@ class TgBotSocketNative {
auto sockFdCloser = std::unique_ptr<int, decltype(&closeFd)>(
&sockfd, &TgBotSocketNative::closeFd);
ABSL_LOG(INFO) << "Using IP: " << std::quoted(config.address, '\'')
<< ", Port: " << kSocketPort;
<< ", Port: " << config.port;
setupSockAddress(&addr);

// Calculate CRC32
Expand All @@ -121,9 +129,51 @@ class TgBotSocketNative {
context.header.checksum = crc;

if (connect(sockfd, reinterpret_cast<sockaddr *>(&addr), len) != 0) {
_callFailed(env, callbackObj, "Failed to connect to server");
if (errno != EINPROGRESS) {
_callFailed(env, callbackObj, "Failed to initiate connect()");
return;
}
}

// Wait for the nonblocking socket's event...
struct pollfd fds{};
fds.events = POLLOUT;
fds.fd = sockfd;
ret = poll(&fds, 1, config.timeout_s * 1000);
if (ret < 0) {
_callFailed(env, callbackObj, "Failed to poll()");
return;
} else if (!(fds.revents & POLLOUT)) {
_callFailed(env, callbackObj, "The server didn't respond");
return;
}

// Get if it failed...
int error = 0;
socklen_t error_len = sizeof(error);
ret = getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &error_len);
if (ret < 0) {
_callFailed(env, callbackObj, "Failed to getsockopt()");
return;
}

// If failed to connect, abort
if (error != 0) {
callFailed(env, callbackObj, "Failed to connect to server");
return;
}

ret = fcntl(sockfd, F_GETFL);
if (ret < 0) {
_callFailed(env, callbackObj, "Failed to get socket fd flags");
return;
}
ret = fcntl(sockfd, F_SETFL, ret & ~O_NONBLOCK);
if (ret < 0) {
_callFailed(env, callbackObj, "Failed to set socket fd blocking");
return;
}

ABSL_LOG(INFO) << "Connected to server";
ret = send(sockfd, &context.header, sizeof(PacketHeader), MSG_NOSIGNAL);
if (ret < 0) {
Expand Down Expand Up @@ -200,13 +250,13 @@ class TgBotSocketNative {
[[maybe_unused]] void setupSockAddress(sockaddr_in *addr) const {
addr->sin_family = AF_INET;
inet_pton(AF_INET, config.address.c_str(), &addr->sin_addr);
addr->sin_port = htons(kSocketPort);
addr->sin_port = htons(config.port);
}
template <>
[[maybe_unused]] void setupSockAddress(sockaddr_in6 *addr) const {
addr->sin6_family = AF_INET6;
inet_pton(AF_INET6, config.address.c_str(), &addr->sin6_addr);
addr->sin6_port = htons(kSocketPort);
addr->sin6_port = htons(config.port);
}

template <Command cmd>
Expand Down Expand Up @@ -242,7 +292,7 @@ void initLogging(JNIEnv *env, jobject thiz) {
}

jboolean changeDestinationInfo(JNIEnv *env, jobject thiz, jstring ipaddr,
jint type) {
jint type, jint port) {
auto sockIntf = TgBotSocketNative::getInstance();
std::string newAddress = Convert<std::string>(env, ipaddr);
SocketConfig config{};
Expand All @@ -259,12 +309,37 @@ jboolean changeDestinationInfo(JNIEnv *env, jobject thiz, jstring ipaddr,
return false;
}
config.address = newAddress;
config.port = port;
ABSL_LOG(INFO) << "Switched to IP " << std::quoted(newAddress, '\'')
<< ", af: " << type;
<< ", af: " << type << ", port: " << port;
sockIntf->setSocketConfig(config);
return true;
}

jobject getCurrentDestinationInfo(JNIEnv *env, jobject thiz) {
auto cf = TgBotSocketNative::getInstance()->getSocketConfig();
jclass info = env->FindClass(SOCKNATIVE_DSTINFO_JAVACLS);
jmethodID ctor = env->GetMethodID(info, "<init>",
"(L" STRING_JAVACLS
";L" SOCKNATIVE_DSTTYPE_JAVACLS ";I)V");
jclass destType = env->FindClass(SOCKNATIVE_DSTTYPE_JAVACLS);
jmethodID valueOf =
env->GetStaticMethodID(destType, "valueOf",
"(L" STRING_JAVACLS ";)"
"L" SOCKNATIVE_DSTTYPE_JAVACLS ";");
jobject destTypeVal;
switch (cf.mode) {
case SocketConfig::USE_IPV4:
destTypeVal = env->CallStaticObjectMethod(
destType, valueOf, Convert<jstring>(env, "IPv4"));
break;
case SocketConfig::USE_IPV6:
destTypeVal = env->CallStaticObjectMethod(
destType, valueOf, Convert<jstring>(env, "IPv6"));
break;
}
return env->NewObject(info, ctor, Convert<jstring>(env, cf.address),
destTypeVal, cf.port);
}
void sendWriteMessageToChatId(JNIEnv *env, jobject thiz, jlong chat_id,
jstring text, jobject callback) {
WriteMsgToChatId data;
Expand Down Expand Up @@ -299,7 +374,8 @@ void downloadFile(JNIEnv *env, jobject thiz, jstring remote_file_path,
jstring local_file_path, jobject callback) {
DownloadFile req{};
copyTo(req.filepath, Convert<std::string>(env, remote_file_path).c_str());
copyTo(req.destfilename, Convert<std::string>(env, local_file_path).c_str());
copyTo(req.destfilename,
Convert<std::string>(env, local_file_path).c_str());
auto pkt = Packet(Command::CMD_DOWNLOAD_FILE, req);
TgBotSocketNative::getInstance()->sendContext(pkt, env, callback);
}
Expand All @@ -308,19 +384,17 @@ void downloadFile(JNIEnv *env, jobject thiz, jstring remote_file_path,
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *__unused reserved) {
static const JNINativeMethod methods[] = {
NATIVE_METHOD(initLogging, "()V"),
NATIVE_METHOD(changeDestinationInfo, "(Ljava/lang/String;I)Z"),
NATIVE_METHOD(changeDestinationInfo, "(L" STRING_JAVACLS ";II)Z"),
NATIVE_METHOD(sendWriteMessageToChatId,
"(JLjava/lang/String;Lcom/royna/tgbotclient/"
"SocketCommandNative$ICommandCallback;)V"),
NATIVE_METHOD(
getUptime,
"(Lcom/royna/tgbotclient/SocketCommandNative$ICommandCallback;)V"),
NATIVE_METHOD(uploadFile,
"(Ljava/lang/String;Ljava/lang/String;Lcom/royna/"
"tgbotclient/SocketCommandNative$ICommandCallback;)V"),
NATIVE_METHOD(downloadFile,
"(Ljava/lang/String;Ljava/lang/String;Lcom/royna/"
"tgbotclient/SocketCommandNative$ICommandCallback;)V")};
return JNI_onLoadDef(vm, "com/royna/tgbotclient/SocketCommandNative",
methods, NATIVE_METHOD_SZ(methods));
"(JL" STRING_JAVACLS ";L" SOCKNATIVE_CMDCB_JAVACLS ";)V"),
NATIVE_METHOD(getUptime, "(L" SOCKNATIVE_CMDCB_JAVACLS ";)V"),
NATIVE_METHOD(uploadFile, "(L" STRING_JAVACLS ";L" STRING_JAVACLS
";L" SOCKNATIVE_CMDCB_JAVACLS ";)V"),
NATIVE_METHOD(downloadFile, "(L" STRING_JAVACLS ";L" STRING_JAVACLS
";L" SOCKNATIVE_CMDCB_JAVACLS ";)V"),
NATIVE_METHOD(getCurrentDestinationInfo,
"()L" SOCKNATIVE_DSTINFO_JAVACLS ";")};

return JNI_onLoadDef(vm, SOCKNATIVE_JAVACLS, methods,
NATIVE_METHOD_SZ(methods));
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@ package com.royna.tgbotclient

import android.app.Application
import com.royna.tgbotclient.ui.settings.TgClientSettings
import com.royna.tgbotclient.util.Logging
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging

class ClientApplication : Application() {
override fun onCreate() {
super.onCreate()
TgClientSettings.loadConfig(this)
System.setProperty("kotlin-logging-to-android-native", "true")
Logging.i { "Application created" }
}
}
Loading

0 comments on commit cf30ccb

Please sign in to comment.