Commit 78f56447 authored by 李朝发's avatar 李朝发

Initial commit

parents
/build
\ No newline at end of file
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
}
android {
namespace 'cn.qmai.socket'
compileSdk 33
defaultConfig {
applicationId "cn.qmai.socket"
minSdk 21
targetSdk 33
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.6.0'
implementation 'com.google.android.material:material:1.8.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}
\ No newline at end of file
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
\ No newline at end of file
package cn.qmai.socket
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("cn.qmai.socket", appContext.packageName)
}
}
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.Socket"
tools:targetApi="31" />
</manifest>
\ No newline at end of file
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
\ No newline at end of file
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.Socket" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>
\ No newline at end of file
<resources>
<string name="app_name">Socket</string>
</resources>
\ No newline at end of file
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.Socket" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older that API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>
\ No newline at end of file
package cn.qmai.socket
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}
\ No newline at end of file
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '7.4.0' apply false
id 'com.android.library' version '7.4.0' apply false
id 'org.jetbrains.kotlin.android' version '1.7.21' apply false
}
\ No newline at end of file
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
\ No newline at end of file
#Mon Jan 30 07:17:40 UTC 2023
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# 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
#
# https://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.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
## This file is automatically generated by Android Studio.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file should *NOT* be checked into Version Control Systems,
# as it contains information specific to your local configuration.
#
# Location of the SDK. This is only used by Gradle.
# For customization when using a Version Control System, please read the
# header note.
sdk.dir=/home/arch/Android/Sdk
\ No newline at end of file
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "Socket"
include ':app'
include ':socket'
\ No newline at end of file
/build
\ No newline at end of file
plugins {
id 'com.android.library'
id 'org.jetbrains.kotlin.android'
id 'maven-publish'
}
android {
namespace 'cn.qmai.socket'
compileSdk 33
defaultConfig {
minSdk 21
targetSdk 33
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation 'net.java.dev.jna:jna:5.13.0@aar'
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.6.0'
implementation 'com.google.android.material:material:1.8.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}
def USERNAME = "lizhaofa"
def PASSWORD = "0oh7bcnZtweAo7EX"
def VERSION = "1.0.0"
def RELEASE_REPOSITORY_URL = "https://hub.zmcms.cn/repository/maven-releases"
afterEvaluate {
publishing {
publications {
release(MavenPublication) {
from components.release
groupId "cn.qmai.socket"
artifactId "socket"
version VERSION
}
}
repositories {
maven {
url = RELEASE_REPOSITORY_URL
credentials {
username = USERNAME
password = PASSWORD
}
}
}
}
}
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
\ No newline at end of file
package cn.qmai.socket
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("cn.qmai.socket.test", appContext.packageName)
}
}
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
\ No newline at end of file
// This file was autogenerated by some hot garbage in the `uniffi` crate.
// Trust me, you don't want to mess with it!
@file:Suppress("NAME_SHADOWING")
package cn.qmai.socket
// Common helper code.
//
// Ideally this would live in a separate .kt file where it can be unittested etc
// in isolation, and perhaps even published as a re-useable package.
//
// However, it's important that the detils of how this helper code works (e.g. the
// way that different builtin types are passed across the FFI) exactly match what's
// expected by the Rust code on the other side of the interface. In practice right
// now that means coming from the exact some version of `uniffi` that was used to
// compile the Rust component. The easiest way to ensure this is to bundle the Kotlin
// helpers directly inline like we're doing here.
import com.sun.jna.Library
import com.sun.jna.Native
import com.sun.jna.Pointer
import com.sun.jna.Structure
import com.sun.jna.ptr.ByReference
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
// This is a helper for safely working with byte buffers returned from the Rust code.
// A rust-owned buffer is represented by its capacity, its current length, and a
// pointer to the underlying data.
@Structure.FieldOrder("capacity", "len", "data")
open class RustBuffer : Structure() {
@JvmField var capacity: Int = 0
@JvmField var len: Int = 0
@JvmField var data: Pointer? = null
class ByValue : RustBuffer(), Structure.ByValue
class ByReference : RustBuffer(), Structure.ByReference
companion object {
internal fun alloc(size: Int = 0) = rustCall() { status ->
_UniFFILib.INSTANCE.ffi_NativeSocket_eac7_rustbuffer_alloc(size, status).also {
if (it.data == null) {
throw RuntimeException("RustBuffer.alloc() returned null data pointer (size=$size)")
}
}
}
internal fun free(buf: RustBuffer.ByValue) = rustCall() { status ->
_UniFFILib.INSTANCE.ffi_NativeSocket_eac7_rustbuffer_free(buf, status)
}
}
@Suppress("TooGenericExceptionThrown")
fun asByteBuffer() =
this.data?.getByteBuffer(0, this.len.toLong())?.also {
it.order(ByteOrder.BIG_ENDIAN)
}
}
/**
* The equivalent of the `*mut RustBuffer` type.
* Required for callbacks taking in an out pointer.
*
* Size is the sum of all values in the struct.
*/
class RustBufferByReference : ByReference(16) {
/**
* Set the pointed-to `RustBuffer` to the given value.
*/
fun setValue(value: RustBuffer.ByValue) {
// NOTE: The offsets are as they are in the C-like struct.
val pointer = getPointer()
pointer.setInt(0, value.capacity)
pointer.setInt(4, value.len)
pointer.setPointer(8, value.data)
}
}
// This is a helper for safely passing byte references into the rust code.
// It's not actually used at the moment, because there aren't many things that you
// can take a direct pointer to in the JVM, and if we're going to copy something
// then we might as well copy it into a `RustBuffer`. But it's here for API
// completeness.
@Structure.FieldOrder("len", "data")
open class ForeignBytes : Structure() {
@JvmField var len: Int = 0
@JvmField var data: Pointer? = null
class ByValue : ForeignBytes(), Structure.ByValue
}
// The FfiConverter interface handles converter types to and from the FFI
//
// All implementing objects should be public to support external types. When a
// type is external we need to import it's FfiConverter.
public interface FfiConverter<KotlinType, FfiType> {
// Convert an FFI type to a Kotlin type
fun lift(value: FfiType): KotlinType
// Convert an Kotlin type to an FFI type
fun lower(value: KotlinType): FfiType
// Read a Kotlin type from a `ByteBuffer`
fun read(buf: ByteBuffer): KotlinType
// Calculate bytes to allocate when creating a `RustBuffer`
//
// This must return at least as many bytes as the write() function will
// write. It can return more bytes than needed, for example when writing
// Strings we can't know the exact bytes needed until we the UTF-8
// encoding, so we pessimistically allocate the largest size possible (3
// bytes per codepoint). Allocating extra bytes is not really a big deal
// because the `RustBuffer` is short-lived.
fun allocationSize(value: KotlinType): Int
// Write a Kotlin type to a `ByteBuffer`
fun write(value: KotlinType, buf: ByteBuffer)
// Lower a value into a `RustBuffer`
//
// This method lowers a value into a `RustBuffer` rather than the normal
// FfiType. It's used by the callback interface code. Callback interface
// returns are always serialized into a `RustBuffer` regardless of their
// normal FFI type.
fun lowerIntoRustBuffer(value: KotlinType): RustBuffer.ByValue {
val rbuf = RustBuffer.alloc(allocationSize(value))
try {
val bbuf = rbuf.data!!.getByteBuffer(0, rbuf.capacity.toLong()).also {
it.order(ByteOrder.BIG_ENDIAN)
}
write(value, bbuf)
rbuf.writeField("len", bbuf.position())
return rbuf
} catch (e: Throwable) {
RustBuffer.free(rbuf)
throw e
}
}
// Lift a value from a `RustBuffer`.
//
// This here mostly because of the symmetry with `lowerIntoRustBuffer()`.
// It's currently only used by the `FfiConverterRustBuffer` class below.
fun liftFromRustBuffer(rbuf: RustBuffer.ByValue): KotlinType {
val byteBuf = rbuf.asByteBuffer()!!
try {
val item = read(byteBuf)
if (byteBuf.hasRemaining()) {
throw RuntimeException("junk remaining in buffer after lifting, something is very wrong!!")
}
return item
} finally {
RustBuffer.free(rbuf)
}
}
}
// FfiConverter that uses `RustBuffer` as the FfiType
public interface FfiConverterRustBuffer<KotlinType> : FfiConverter<KotlinType, RustBuffer.ByValue> {
override fun lift(value: RustBuffer.ByValue) = liftFromRustBuffer(value)
override fun lower(value: KotlinType) = lowerIntoRustBuffer(value)
}
// A handful of classes and functions to support the generated data structures.
// This would be a good candidate for isolating in its own ffi-support lib.
// Error runtime.
@Structure.FieldOrder("code", "error_buf")
internal open class RustCallStatus : Structure() {
@JvmField var code: Int = 0
@JvmField var error_buf: RustBuffer.ByValue = RustBuffer.ByValue()
fun isSuccess(): Boolean {
return code == 0
}
fun isError(): Boolean {
return code == 1
}
fun isPanic(): Boolean {
return code == 2
}
}
class InternalException(message: String) : Exception(message)
// Each top-level error class has a companion object that can lift the error from the call status's rust buffer
interface CallStatusErrorHandler<E> {
fun lift(error_buf: RustBuffer.ByValue): E
}
// Helpers for calling Rust
// In practice we usually need to be synchronized to call this safely, so it doesn't
// synchronize itself
// Call a rust function that returns a Result<>. Pass in the Error class companion that corresponds to the Err
private inline fun <U, E : Exception> rustCallWithError(errorHandler: CallStatusErrorHandler<E>, callback: (RustCallStatus) -> U): U {
var status = RustCallStatus()
val return_value = callback(status)
if (status.isSuccess()) {
return return_value
} else if (status.isError()) {
throw errorHandler.lift(status.error_buf)
} else if (status.isPanic()) {
// when the rust code sees a panic, it tries to construct a rustbuffer
// with the message. but if that code panics, then it just sends back
// an empty buffer.
if (status.error_buf.len > 0) {
throw InternalException(FfiConverterString.lift(status.error_buf))
} else {
throw InternalException("Rust panic")
}
} else {
throw InternalException("Unknown rust call status: $status.code")
}
}
// CallStatusErrorHandler implementation for times when we don't expect a CALL_ERROR
object NullCallStatusErrorHandler : CallStatusErrorHandler<InternalException> {
override fun lift(error_buf: RustBuffer.ByValue): InternalException {
RustBuffer.free(error_buf)
return InternalException("Unexpected CALL_ERROR")
}
}
// Call a rust function that returns a plain value
private inline fun <U> rustCall(callback: (RustCallStatus) -> U): U {
return rustCallWithError(NullCallStatusErrorHandler, callback)
}
// Contains loading, initialization code,
// and the FFI Function declarations in a com.sun.jna.Library.
@Synchronized
private fun findLibraryName(componentName: String): String {
val libOverride = System.getProperty("uniffi.component.$componentName.libraryOverride")
if (libOverride != null) {
return libOverride
}
return "socket"
}
private inline fun <reified Lib : Library> loadIndirect(
componentName: String,
): Lib {
return Native.load<Lib>(findLibraryName(componentName), Lib::class.java)
}
// A JNA Library to expose the extern-C FFI definitions.
// This is an implementation detail which will be called internally by the public API.
internal interface _UniFFILib : Library {
companion object {
internal val INSTANCE: _UniFFILib by lazy {
loadIndirect<_UniFFILib>(componentName = "NativeSocket")
.also { lib: _UniFFILib ->
FfiConverterTypeLeastOnceListener.register(lib)
FfiConverterTypeMostOnceListener.register(lib)
}
}
}
fun ffi_NativeSocket_eac7_NativeSocketBuilder_object_free(
`ptr`: Pointer,
_uniffi_out_err: RustCallStatus,
): Unit
fun NativeSocket_eac7_NativeSocketBuilder_new(
_uniffi_out_err: RustCallStatus,
): Pointer
fun NativeSocket_eac7_NativeSocketBuilder_host(
`ptr`: Pointer,
`host`: RustBuffer.ByValue,
_uniffi_out_err: RustCallStatus,
): Pointer
fun NativeSocket_eac7_NativeSocketBuilder_token(
`ptr`: Pointer,
`token`: RustBuffer.ByValue,
_uniffi_out_err: RustCallStatus,
): Pointer
fun NativeSocket_eac7_NativeSocketBuilder_brand(
`ptr`: Pointer,
`brand`: Int,
_uniffi_out_err: RustCallStatus,
): Pointer
fun NativeSocket_eac7_NativeSocketBuilder_store(
`ptr`: Pointer,
`store`: Int,
_uniffi_out_err: RustCallStatus,
): Pointer
fun NativeSocket_eac7_NativeSocketBuilder_device(
`ptr`: Pointer,
`device`: RustBuffer.ByValue,
_uniffi_out_err: RustCallStatus,
): Pointer
fun NativeSocket_eac7_NativeSocketBuilder_type(
`ptr`: Pointer,
`type`: Int,
_uniffi_out_err: RustCallStatus,
): Pointer
fun NativeSocket_eac7_NativeSocketBuilder_topic(
`ptr`: Pointer,
`topic`: RustBuffer.ByValue,
_uniffi_out_err: RustCallStatus,
): Pointer
fun NativeSocket_eac7_NativeSocketBuilder_clean_start(
`ptr`: Pointer,
`cleanStart`: Byte,
_uniffi_out_err: RustCallStatus,
): Pointer
fun NativeSocket_eac7_NativeSocketBuilder_cache_dir(
`ptr`: Pointer,
`cacheDir`: RustBuffer.ByValue,
_uniffi_out_err: RustCallStatus,
): Pointer
fun NativeSocket_eac7_NativeSocketBuilder_log_level(
`ptr`: Pointer,
`logLevel`: RustBuffer.ByValue,
_uniffi_out_err: RustCallStatus,
): Pointer
fun NativeSocket_eac7_NativeSocketBuilder_sentry(
`ptr`: Pointer,
`sentry`: Byte,
_uniffi_out_err: RustCallStatus,
): Pointer
fun NativeSocket_eac7_NativeSocketBuilder_build(
`ptr`: Pointer,
_uniffi_out_err: RustCallStatus,
): RustBuffer.ByValue
fun ffi_NativeSocket_eac7_NativeSocket_object_free(
`ptr`: Pointer,
_uniffi_out_err: RustCallStatus,
): Unit
fun NativeSocket_eac7_NativeSocket_start(
`ptr`: Pointer,
`mostOnce`: RustBuffer.ByValue,
`leastOnce`: RustBuffer.ByValue,
_uniffi_out_err: RustCallStatus,
): Unit
fun ffi_NativeSocket_eac7_MostOnceListener_init_callback(
`callbackStub`: ForeignCallback,
_uniffi_out_err: RustCallStatus,
): Unit
fun ffi_NativeSocket_eac7_LeastOnceListener_init_callback(
`callbackStub`: ForeignCallback,
_uniffi_out_err: RustCallStatus,
): Unit
fun ffi_NativeSocket_eac7_rustbuffer_alloc(
`size`: Int,
_uniffi_out_err: RustCallStatus,
): RustBuffer.ByValue
fun ffi_NativeSocket_eac7_rustbuffer_from_bytes(
`bytes`: ForeignBytes.ByValue,
_uniffi_out_err: RustCallStatus,
): RustBuffer.ByValue
fun ffi_NativeSocket_eac7_rustbuffer_free(
`buf`: RustBuffer.ByValue,
_uniffi_out_err: RustCallStatus,
): Unit
fun ffi_NativeSocket_eac7_rustbuffer_reserve(
`buf`: RustBuffer.ByValue,
`additional`: Int,
_uniffi_out_err: RustCallStatus,
): RustBuffer.ByValue
}
// Public interface members begin here.
public object FfiConverterUByte : FfiConverter<UByte, Byte> {
override fun lift(value: Byte): UByte {
return value.toUByte()
}
override fun read(buf: ByteBuffer): UByte {
return lift(buf.get())
}
override fun lower(value: UByte): Byte {
return value.toByte()
}
override fun allocationSize(value: UByte) = 1
override fun write(value: UByte, buf: ByteBuffer) {
buf.put(value.toByte())
}
}
public object FfiConverterInt : FfiConverter<Int, Int> {
override fun lift(value: Int): Int {
return value
}
override fun read(buf: ByteBuffer): Int {
return buf.getInt()
}
override fun lower(value: Int): Int {
return value
}
override fun allocationSize(value: Int) = 4
override fun write(value: Int, buf: ByteBuffer) {
buf.putInt(value)
}
}
public object FfiConverterLong : FfiConverter<Long, Long> {
override fun lift(value: Long): Long {
return value
}
override fun read(buf: ByteBuffer): Long {
return buf.getLong()
}
override fun lower(value: Long): Long {
return value
}
override fun allocationSize(value: Long) = 8
override fun write(value: Long, buf: ByteBuffer) {
buf.putLong(value)
}
}
public object FfiConverterBoolean : FfiConverter<Boolean, Byte> {
override fun lift(value: Byte): Boolean {
return value.toInt() != 0
}
override fun read(buf: ByteBuffer): Boolean {
return lift(buf.get())
}
override fun lower(value: Boolean): Byte {
return if (value) 1.toByte() else 0.toByte()
}
override fun allocationSize(value: Boolean) = 1
override fun write(value: Boolean, buf: ByteBuffer) {
buf.put(lower(value))
}
}
public object FfiConverterString : FfiConverter<String, RustBuffer.ByValue> {
// Note: we don't inherit from FfiConverterRustBuffer, because we use a
// special encoding when lowering/lifting. We can use `RustBuffer.len` to
// store our length and avoid writing it out to the buffer.
override fun lift(value: RustBuffer.ByValue): String {
try {
val byteArr = ByteArray(value.len)
value.asByteBuffer()!!.get(byteArr)
return byteArr.toString(Charsets.UTF_8)
} finally {
RustBuffer.free(value)
}
}
override fun read(buf: ByteBuffer): String {
val len = buf.getInt()
val byteArr = ByteArray(len)
buf.get(byteArr)
return byteArr.toString(Charsets.UTF_8)
}
override fun lower(value: String): RustBuffer.ByValue {
val byteArr = value.toByteArray(Charsets.UTF_8)
// Ideally we'd pass these bytes to `ffi_bytebuffer_from_bytes`, but doing so would require us
// to copy them into a JNA `Memory`. So we might as well directly copy them into a `RustBuffer`.
val rbuf = RustBuffer.alloc(byteArr.size)
rbuf.asByteBuffer()!!.put(byteArr)
return rbuf
}
// We aren't sure exactly how many bytes our string will be once it's UTF-8
// encoded. Allocate 3 bytes per unicode codepoint which will always be
// enough.
override fun allocationSize(value: String): Int {
val sizeForLength = 4
val sizeForString = value.length * 3
return sizeForLength + sizeForString
}
override fun write(value: String, buf: ByteBuffer) {
val byteArr = value.toByteArray(Charsets.UTF_8)
buf.putInt(byteArr.size)
buf.put(byteArr)
}
}
// Interface implemented by anything that can contain an object reference.
//
// Such types expose a `destroy()` method that must be called to cleanly
// dispose of the contained objects. Failure to call this method may result
// in memory leaks.
//
// The easiest way to ensure this method is called is to use the `.use`
// helper method to execute a block and destroy the object at the end.
interface Disposable {
fun destroy()
companion object {
fun destroy(vararg args: Any?) {
args.filterIsInstance<Disposable>()
.forEach(Disposable::destroy)
}
}
}
inline fun <T : Disposable?, R> T.use(block: (T) -> R) =
try {
block(this)
} finally {
try {
// N.B. our implementation is on the nullable type `Disposable?`.
this?.destroy()
} catch (e: Throwable) {
// swallow
}
}
// The base class for all UniFFI Object types.
//
// This class provides core operations for working with the Rust `Arc<T>` pointer to
// the live Rust struct on the other side of the FFI.
//
// There's some subtlety here, because we have to be careful not to operate on a Rust
// struct after it has been dropped, and because we must expose a public API for freeing
// the Kotlin wrapper object in lieu of reliable finalizers. The core requirements are:
//
// * Each `FFIObject` instance holds an opaque pointer to the underlying Rust struct.
// Method calls need to read this pointer from the object's state and pass it in to
// the Rust FFI.
//
// * When an `FFIObject` is no longer needed, its pointer should be passed to a
// special destructor function provided by the Rust FFI, which will drop the
// underlying Rust struct.
//
// * Given an `FFIObject` instance, calling code is expected to call the special
// `destroy` method in order to free it after use, either by calling it explicitly
// or by using a higher-level helper like the `use` method. Failing to do so will
// leak the underlying Rust struct.
//
// * We can't assume that calling code will do the right thing, and must be prepared
// to handle Kotlin method calls executing concurrently with or even after a call to
// `destroy`, and to handle multiple (possibly concurrent!) calls to `destroy`.
//
// * We must never allow Rust code to operate on the underlying Rust struct after
// the destructor has been called, and must never call the destructor more than once.
// Doing so may trigger memory unsafety.
//
// If we try to implement this with mutual exclusion on access to the pointer, there is the
// possibility of a race between a method call and a concurrent call to `destroy`:
//
// * Thread A starts a method call, reads the value of the pointer, but is interrupted
// before it can pass the pointer over the FFI to Rust.
// * Thread B calls `destroy` and frees the underlying Rust struct.
// * Thread A resumes, passing the already-read pointer value to Rust and triggering
// a use-after-free.
//
// One possible solution would be to use a `ReadWriteLock`, with each method call taking
// a read lock (and thus allowed to run concurrently) and the special `destroy` method
// taking a write lock (and thus blocking on live method calls). However, we aim not to
// generate methods with any hidden blocking semantics, and a `destroy` method that might
// block if called incorrectly seems to meet that bar.
//
// So, we achieve our goals by giving each `FFIObject` an associated `AtomicLong` counter to track
// the number of in-flight method calls, and an `AtomicBoolean` flag to indicate whether `destroy`
// has been called. These are updated according to the following rules:
//
// * The initial value of the counter is 1, indicating a live object with no in-flight calls.
// The initial value for the flag is false.
//
// * At the start of each method call, we atomically check the counter.
// If it is 0 then the underlying Rust struct has already been destroyed and the call is aborted.
// If it is nonzero them we atomically increment it by 1 and proceed with the method call.
//
// * At the end of each method call, we atomically decrement and check the counter.
// If it has reached zero then we destroy the underlying Rust struct.
//
// * When `destroy` is called, we atomically flip the flag from false to true.
// If the flag was already true we silently fail.
// Otherwise we atomically decrement and check the counter.
// If it has reached zero then we destroy the underlying Rust struct.
//
// Astute readers may observe that this all sounds very similar to the way that Rust's `Arc<T>` works,
// and indeed it is, with the addition of a flag to guard against multiple calls to `destroy`.
//
// The overall effect is that the underlying Rust struct is destroyed only when `destroy` has been
// called *and* all in-flight method calls have completed, avoiding violating any of the expectations
// of the underlying Rust code.
//
// In the future we may be able to replace some of this with automatic finalization logic, such as using
// the new "Cleaner" functionaility in Java 9. The above scheme has been designed to work even if `destroy` is
// invoked by garbage-collection machinery rather than by calling code (which by the way, it's apparently also
// possible for the JVM to finalize an object while there is an in-flight call to one of its methods [1],
// so there would still be some complexity here).
//
// Sigh...all of this for want of a robust finalization mechanism.
//
// [1] https://stackoverflow.com/questions/24376768/can-java-finalize-an-object-when-it-is-still-in-scope/24380219
//
abstract class FFIObject(
protected val pointer: Pointer,
) : Disposable, AutoCloseable {
private val wasDestroyed = AtomicBoolean(false)
private val callCounter = AtomicLong(1)
protected open fun freeRustArcPtr() {
// To be overridden in subclasses.
}
override fun destroy() {
// Only allow a single call to this method.
// TODO: maybe we should log a warning if called more than once?
if (this.wasDestroyed.compareAndSet(false, true)) {
// This decrement always matches the initial count of 1 given at creation time.
if (this.callCounter.decrementAndGet() == 0L) {
this.freeRustArcPtr()
}
}
}
@Synchronized
override fun close() {
this.destroy()
}
internal inline fun <R> callWithPointer(block: (ptr: Pointer) -> R): R {
// Check and increment the call counter, to keep the object alive.
// This needs a compare-and-set retry loop in case of concurrent updates.
do {
val c = this.callCounter.get()
if (c == 0L) {
throw IllegalStateException("${this.javaClass.simpleName} object has already been destroyed")
}
if (c == Long.MAX_VALUE) {
throw IllegalStateException("${this.javaClass.simpleName} call counter would overflow")
}
} while (!this.callCounter.compareAndSet(c, c + 1L))
// Now we can safely do the method call without the pointer being freed concurrently.
try {
return block(this.pointer)
} finally {
// This decrement aways matches the increment we performed above.
if (this.callCounter.decrementAndGet() == 0L) {
this.freeRustArcPtr()
}
}
}
}
public interface NativeSocketInterface {
fun `start`(`mostOnce`: MostOnceListener?, `leastOnce`: LeastOnceListener?)
}
class NativeSocket(
pointer: Pointer,
) : FFIObject(pointer), NativeSocketInterface {
/**
* Disconnect the object from the underlying Rust object.
*
* It can be called more than once, but once called, interacting with the object
* causes an `IllegalStateException`.
*
* Clients **must** call this method once done with the object, or cause a memory leak.
*/
protected override fun freeRustArcPtr() {
rustCall() { status ->
_UniFFILib.INSTANCE.ffi_NativeSocket_eac7_NativeSocket_object_free(this.pointer, status)
}
}
override fun `start`(`mostOnce`: MostOnceListener?, `leastOnce`: LeastOnceListener?) =
callWithPointer {
rustCall() { _status ->
_UniFFILib.INSTANCE.NativeSocket_eac7_NativeSocket_start(it, FfiConverterOptionalTypeMostOnceListener.lower(`mostOnce`), FfiConverterOptionalTypeLeastOnceListener.lower(`leastOnce`), _status)
}
}
}
public object FfiConverterTypeNativeSocket : FfiConverter<NativeSocket, Pointer> {
override fun lower(value: NativeSocket): Pointer = value.callWithPointer { it }
override fun lift(value: Pointer): NativeSocket {
return NativeSocket(value)
}
override fun read(buf: ByteBuffer): NativeSocket {
// The Rust code always writes pointers as 8 bytes, and will
// fail to compile if they don't fit.
return lift(Pointer(buf.getLong()))
}
override fun allocationSize(value: NativeSocket) = 8
override fun write(value: NativeSocket, buf: ByteBuffer) {
// The Rust code always expects pointers written as 8 bytes,
// and will fail to compile if they don't fit.
buf.putLong(Pointer.nativeValue(lower(value)))
}
}
public interface NativeSocketBuilderInterface {
fun `host`(`host`: String): NativeSocketBuilder
fun `token`(`token`: String): NativeSocketBuilder
fun `brand`(`brand`: Int): NativeSocketBuilder
fun `store`(`store`: Int): NativeSocketBuilder
fun `device`(`device`: String): NativeSocketBuilder
fun `type`(`type`: Int): NativeSocketBuilder
fun `topic`(`topic`: List<String>): NativeSocketBuilder
fun `cleanStart`(`cleanStart`: Boolean): NativeSocketBuilder
fun `cacheDir`(`cacheDir`: String): NativeSocketBuilder
fun `logLevel`(`logLevel`: LogLevel): NativeSocketBuilder
fun `sentry`(`sentry`: Boolean): NativeSocketBuilder
fun `build`(): NativeSocket?
}
class NativeSocketBuilder(
pointer: Pointer,
) : FFIObject(pointer), NativeSocketBuilderInterface {
constructor() :
this(
rustCall() { _status ->
_UniFFILib.INSTANCE.NativeSocket_eac7_NativeSocketBuilder_new(_status)
},
)
/**
* Disconnect the object from the underlying Rust object.
*
* It can be called more than once, but once called, interacting with the object
* causes an `IllegalStateException`.
*
* Clients **must** call this method once done with the object, or cause a memory leak.
*/
protected override fun freeRustArcPtr() {
rustCall() { status ->
_UniFFILib.INSTANCE.ffi_NativeSocket_eac7_NativeSocketBuilder_object_free(this.pointer, status)
}
}
override fun `host`(`host`: String): NativeSocketBuilder =
callWithPointer {
rustCall() { _status ->
_UniFFILib.INSTANCE.NativeSocket_eac7_NativeSocketBuilder_host(it, FfiConverterString.lower(`host`), _status)
}
}.let {
FfiConverterTypeNativeSocketBuilder.lift(it)
}
override fun `token`(`token`: String): NativeSocketBuilder =
callWithPointer {
rustCall() { _status ->
_UniFFILib.INSTANCE.NativeSocket_eac7_NativeSocketBuilder_token(it, FfiConverterString.lower(`token`), _status)
}
}.let {
FfiConverterTypeNativeSocketBuilder.lift(it)
}
override fun `brand`(`brand`: Int): NativeSocketBuilder =
callWithPointer {
rustCall() { _status ->
_UniFFILib.INSTANCE.NativeSocket_eac7_NativeSocketBuilder_brand(it, FfiConverterInt.lower(`brand`), _status)
}
}.let {
FfiConverterTypeNativeSocketBuilder.lift(it)
}
override fun `store`(`store`: Int): NativeSocketBuilder =
callWithPointer {
rustCall() { _status ->
_UniFFILib.INSTANCE.NativeSocket_eac7_NativeSocketBuilder_store(it, FfiConverterInt.lower(`store`), _status)
}
}.let {
FfiConverterTypeNativeSocketBuilder.lift(it)
}
override fun `device`(`device`: String): NativeSocketBuilder =
callWithPointer {
rustCall() { _status ->
_UniFFILib.INSTANCE.NativeSocket_eac7_NativeSocketBuilder_device(it, FfiConverterString.lower(`device`), _status)
}
}.let {
FfiConverterTypeNativeSocketBuilder.lift(it)
}
override fun `type`(`type`: Int): NativeSocketBuilder =
callWithPointer {
rustCall() { _status ->
_UniFFILib.INSTANCE.NativeSocket_eac7_NativeSocketBuilder_type(it, FfiConverterInt.lower(`type`), _status)
}
}.let {
FfiConverterTypeNativeSocketBuilder.lift(it)
}
override fun `topic`(`topic`: List<String>): NativeSocketBuilder =
callWithPointer {
rustCall() { _status ->
_UniFFILib.INSTANCE.NativeSocket_eac7_NativeSocketBuilder_topic(it, FfiConverterSequenceString.lower(`topic`), _status)
}
}.let {
FfiConverterTypeNativeSocketBuilder.lift(it)
}
override fun `cleanStart`(`cleanStart`: Boolean): NativeSocketBuilder =
callWithPointer {
rustCall() { _status ->
_UniFFILib.INSTANCE.NativeSocket_eac7_NativeSocketBuilder_clean_start(it, FfiConverterBoolean.lower(`cleanStart`), _status)
}
}.let {
FfiConverterTypeNativeSocketBuilder.lift(it)
}
override fun `cacheDir`(`cacheDir`: String): NativeSocketBuilder =
callWithPointer {
rustCall() { _status ->
_UniFFILib.INSTANCE.NativeSocket_eac7_NativeSocketBuilder_cache_dir(it, FfiConverterString.lower(`cacheDir`), _status)
}
}.let {
FfiConverterTypeNativeSocketBuilder.lift(it)
}
override fun `logLevel`(`logLevel`: LogLevel): NativeSocketBuilder =
callWithPointer {
rustCall() { _status ->
_UniFFILib.INSTANCE.NativeSocket_eac7_NativeSocketBuilder_log_level(it, FfiConverterTypeLogLevel.lower(`logLevel`), _status)
}
}.let {
FfiConverterTypeNativeSocketBuilder.lift(it)
}
override fun `sentry`(`sentry`: Boolean): NativeSocketBuilder =
callWithPointer {
rustCall() { _status ->
_UniFFILib.INSTANCE.NativeSocket_eac7_NativeSocketBuilder_sentry(it, FfiConverterBoolean.lower(`sentry`), _status)
}
}.let {
FfiConverterTypeNativeSocketBuilder.lift(it)
}
override fun `build`(): NativeSocket? =
callWithPointer {
rustCall() { _status ->
_UniFFILib.INSTANCE.NativeSocket_eac7_NativeSocketBuilder_build(it, _status)
}
}.let {
FfiConverterOptionalTypeNativeSocket.lift(it)
}
}
public object FfiConverterTypeNativeSocketBuilder : FfiConverter<NativeSocketBuilder, Pointer> {
override fun lower(value: NativeSocketBuilder): Pointer = value.callWithPointer { it }
override fun lift(value: Pointer): NativeSocketBuilder {
return NativeSocketBuilder(value)
}
override fun read(buf: ByteBuffer): NativeSocketBuilder {
// The Rust code always writes pointers as 8 bytes, and will
// fail to compile if they don't fit.
return lift(Pointer(buf.getLong()))
}
override fun allocationSize(value: NativeSocketBuilder) = 8
override fun write(value: NativeSocketBuilder, buf: ByteBuffer) {
// The Rust code always expects pointers written as 8 bytes,
// and will fail to compile if they don't fit.
buf.putLong(Pointer.nativeValue(lower(value)))
}
}
data class Message(
var `id`: Long,
var `data`: List<UByte>?,
)
public object FfiConverterTypeMessage : FfiConverterRustBuffer<Message> {
override fun read(buf: ByteBuffer): Message {
return Message(
FfiConverterLong.read(buf),
FfiConverterOptionalSequenceUByte.read(buf),
)
}
override fun allocationSize(value: Message) = (
FfiConverterLong.allocationSize(value.`id`) +
FfiConverterOptionalSequenceUByte.allocationSize(value.`data`)
)
override fun write(value: Message, buf: ByteBuffer) {
FfiConverterLong.write(value.`id`, buf)
FfiConverterOptionalSequenceUByte.write(value.`data`, buf)
}
}
enum class LogLevel {
ERROR, WARN, INFO, DEBUG, TRACE
}
public object FfiConverterTypeLogLevel : FfiConverterRustBuffer<LogLevel> {
override fun read(buf: ByteBuffer) = try {
LogLevel.values()[buf.getInt() - 1]
} catch (e: IndexOutOfBoundsException) {
throw RuntimeException("invalid enum value, something is very wrong!!", e)
}
override fun allocationSize(value: LogLevel) = 4
override fun write(value: LogLevel, buf: ByteBuffer) {
buf.putInt(value.ordinal + 1)
}
}
internal typealias Handle = Long
internal class ConcurrentHandleMap<T>(
private val leftMap: MutableMap<Handle, T> = mutableMapOf(),
private val rightMap: MutableMap<T, Handle> = mutableMapOf(),
) {
private val lock = java.util.concurrent.locks.ReentrantLock()
private val currentHandle = AtomicLong(0L)
private val stride = 1L
fun insert(obj: T): Handle =
lock.withLock {
rightMap[obj]
?: currentHandle.getAndAdd(stride)
.also { handle ->
leftMap[handle] = obj
rightMap[obj] = handle
}
}
fun get(handle: Handle) = lock.withLock {
leftMap[handle]
}
fun delete(handle: Handle) {
this.remove(handle)
}
fun remove(handle: Handle): T? =
lock.withLock {
leftMap.remove(handle)?.let { obj ->
rightMap.remove(obj)
obj
}
}
}
interface ForeignCallback : com.sun.jna.Callback {
public fun invoke(handle: Handle, method: Int, args: RustBuffer.ByValue, outBuf: RustBufferByReference): Int
}
// Magic number for the Rust proxy to call using the same mechanism as every other method,
// to free the callback once it's dropped by Rust.
internal const val IDX_CALLBACK_FREE = 0
public abstract class FfiConverterCallbackInterface<CallbackInterface>(
protected val foreignCallback: ForeignCallback,
) : FfiConverter<CallbackInterface, Handle> {
private val handleMap = ConcurrentHandleMap<CallbackInterface>()
// Registers the foreign callback with the Rust side.
// This method is generated for each callback interface.
internal abstract fun register(lib: _UniFFILib)
fun drop(handle: Handle): RustBuffer.ByValue {
return handleMap.remove(handle).let { RustBuffer.ByValue() }
}
override fun lift(value: Handle): CallbackInterface {
return handleMap.get(value) ?: throw InternalException("No callback in handlemap; this is a Uniffi bug")
}
override fun read(buf: ByteBuffer) = lift(buf.getLong())
override fun lower(value: CallbackInterface) =
handleMap.insert(value).also {
assert(handleMap.get(it) === value) { "Handle map is not returning the object we just placed there. This is a bug in the HandleMap." }
}
override fun allocationSize(value: CallbackInterface) = 8
override fun write(value: CallbackInterface, buf: ByteBuffer) {
buf.putLong(lower(value))
}
}
// Declaration and FfiConverters for LeastOnceListener Callback Interface
public interface LeastOnceListener {
fun `consumerMessage`(`message`: Message)
}
// The ForeignCallback that is passed to Rust.
internal class ForeignCallbackTypeLeastOnceListener : ForeignCallback {
@Suppress("TooGenericExceptionCaught")
override fun invoke(handle: Handle, method: Int, args: RustBuffer.ByValue, outBuf: RustBufferByReference): Int {
val cb = FfiConverterTypeLeastOnceListener.lift(handle)
return when (method) {
IDX_CALLBACK_FREE -> {
FfiConverterTypeLeastOnceListener.drop(handle)
// No return value.
// See docs of ForeignCallback in `uniffi/src/ffi/foreigncallbacks.rs`
0
}
1 -> {
// Call the method, write to outBuf and return a status code
// See docs of ForeignCallback in `uniffi/src/ffi/foreigncallbacks.rs` for info
try {
val buffer = this.`invokeConsumerMessage`(cb, args)
// Success
outBuf.setValue(buffer)
1
} catch (e: Throwable) {
// Unexpected error
try {
// Try to serialize the error into a string
outBuf.setValue(FfiConverterString.lower(e.toString()))
} catch (e: Throwable) {
// If that fails, then it's time to give up and just return
}
-1
}
}
else -> {
// An unexpected error happened.
// See docs of ForeignCallback in `uniffi/src/ffi/foreigncallbacks.rs`
try {
// Try to serialize the error into a string
outBuf.setValue(FfiConverterString.lower("Invalid Callback index"))
} catch (e: Throwable) {
// If that fails, then it's time to give up and just return
}
-1
}
}
}
private fun `invokeConsumerMessage`(kotlinCallbackInterface: LeastOnceListener, args: RustBuffer.ByValue): RustBuffer.ByValue =
try {
val buf = args.asByteBuffer() ?: throw InternalException("No ByteBuffer in RustBuffer; this is a Uniffi bug")
kotlinCallbackInterface.`consumerMessage`(
FfiConverterTypeMessage.read(buf),
)
.let { RustBuffer.ByValue() }
// TODO catch errors and report them back to Rust.
// https://github.com/mozilla/uniffi-rs/issues/351
} finally {
RustBuffer.free(args)
}
}
// The ffiConverter which transforms the Callbacks in to Handles to pass to Rust.
public object FfiConverterTypeLeastOnceListener : FfiConverterCallbackInterface<LeastOnceListener>(
foreignCallback = ForeignCallbackTypeLeastOnceListener(),
) {
override fun register(lib: _UniFFILib) {
rustCall() { status ->
lib.ffi_NativeSocket_eac7_LeastOnceListener_init_callback(this.foreignCallback, status)
}
}
}
// Declaration and FfiConverters for MostOnceListener Callback Interface
public interface MostOnceListener {
fun `consumerMessage`(`message`: Message)
}
// The ForeignCallback that is passed to Rust.
internal class ForeignCallbackTypeMostOnceListener : ForeignCallback {
@Suppress("TooGenericExceptionCaught")
override fun invoke(handle: Handle, method: Int, args: RustBuffer.ByValue, outBuf: RustBufferByReference): Int {
val cb = FfiConverterTypeMostOnceListener.lift(handle)
return when (method) {
IDX_CALLBACK_FREE -> {
FfiConverterTypeMostOnceListener.drop(handle)
// No return value.
// See docs of ForeignCallback in `uniffi/src/ffi/foreigncallbacks.rs`
0
}
1 -> {
// Call the method, write to outBuf and return a status code
// See docs of ForeignCallback in `uniffi/src/ffi/foreigncallbacks.rs` for info
try {
val buffer = this.`invokeConsumerMessage`(cb, args)
// Success
outBuf.setValue(buffer)
1
} catch (e: Throwable) {
// Unexpected error
try {
// Try to serialize the error into a string
outBuf.setValue(FfiConverterString.lower(e.toString()))
} catch (e: Throwable) {
// If that fails, then it's time to give up and just return
}
-1
}
}
else -> {
// An unexpected error happened.
// See docs of ForeignCallback in `uniffi/src/ffi/foreigncallbacks.rs`
try {
// Try to serialize the error into a string
outBuf.setValue(FfiConverterString.lower("Invalid Callback index"))
} catch (e: Throwable) {
// If that fails, then it's time to give up and just return
}
-1
}
}
}
private fun `invokeConsumerMessage`(kotlinCallbackInterface: MostOnceListener, args: RustBuffer.ByValue): RustBuffer.ByValue =
try {
val buf = args.asByteBuffer() ?: throw InternalException("No ByteBuffer in RustBuffer; this is a Uniffi bug")
kotlinCallbackInterface.`consumerMessage`(
FfiConverterTypeMessage.read(buf),
)
.let { RustBuffer.ByValue() }
// TODO catch errors and report them back to Rust.
// https://github.com/mozilla/uniffi-rs/issues/351
} finally {
RustBuffer.free(args)
}
}
// The ffiConverter which transforms the Callbacks in to Handles to pass to Rust.
public object FfiConverterTypeMostOnceListener : FfiConverterCallbackInterface<MostOnceListener>(
foreignCallback = ForeignCallbackTypeMostOnceListener(),
) {
override fun register(lib: _UniFFILib) {
rustCall() { status ->
lib.ffi_NativeSocket_eac7_MostOnceListener_init_callback(this.foreignCallback, status)
}
}
}
public object FfiConverterOptionalTypeNativeSocket : FfiConverterRustBuffer<NativeSocket?> {
override fun read(buf: ByteBuffer): NativeSocket? {
if (buf.get().toInt() == 0) {
return null
}
return FfiConverterTypeNativeSocket.read(buf)
}
override fun allocationSize(value: NativeSocket?): Int {
if (value == null) {
return 1
} else {
return 1 + FfiConverterTypeNativeSocket.allocationSize(value)
}
}
override fun write(value: NativeSocket?, buf: ByteBuffer) {
if (value == null) {
buf.put(0)
} else {
buf.put(1)
FfiConverterTypeNativeSocket.write(value, buf)
}
}
}
public object FfiConverterOptionalTypeLeastOnceListener : FfiConverterRustBuffer<LeastOnceListener?> {
override fun read(buf: ByteBuffer): LeastOnceListener? {
if (buf.get().toInt() == 0) {
return null
}
return FfiConverterTypeLeastOnceListener.read(buf)
}
override fun allocationSize(value: LeastOnceListener?): Int {
if (value == null) {
return 1
} else {
return 1 + FfiConverterTypeLeastOnceListener.allocationSize(value)
}
}
override fun write(value: LeastOnceListener?, buf: ByteBuffer) {
if (value == null) {
buf.put(0)
} else {
buf.put(1)
FfiConverterTypeLeastOnceListener.write(value, buf)
}
}
}
public object FfiConverterOptionalTypeMostOnceListener : FfiConverterRustBuffer<MostOnceListener?> {
override fun read(buf: ByteBuffer): MostOnceListener? {
if (buf.get().toInt() == 0) {
return null
}
return FfiConverterTypeMostOnceListener.read(buf)
}
override fun allocationSize(value: MostOnceListener?): Int {
if (value == null) {
return 1
} else {
return 1 + FfiConverterTypeMostOnceListener.allocationSize(value)
}
}
override fun write(value: MostOnceListener?, buf: ByteBuffer) {
if (value == null) {
buf.put(0)
} else {
buf.put(1)
FfiConverterTypeMostOnceListener.write(value, buf)
}
}
}
public object FfiConverterOptionalSequenceUByte : FfiConverterRustBuffer<List<UByte>?> {
override fun read(buf: ByteBuffer): List<UByte>? {
if (buf.get().toInt() == 0) {
return null
}
return FfiConverterSequenceUByte.read(buf)
}
override fun allocationSize(value: List<UByte>?): Int {
if (value == null) {
return 1
} else {
return 1 + FfiConverterSequenceUByte.allocationSize(value)
}
}
override fun write(value: List<UByte>?, buf: ByteBuffer) {
if (value == null) {
buf.put(0)
} else {
buf.put(1)
FfiConverterSequenceUByte.write(value, buf)
}
}
}
public object FfiConverterSequenceUByte : FfiConverterRustBuffer<List<UByte>> {
override fun read(buf: ByteBuffer): List<UByte> {
val len = buf.getInt()
return List<UByte>(len) {
FfiConverterUByte.read(buf)
}
}
override fun allocationSize(value: List<UByte>): Int {
val sizeForLength = 4
val sizeForItems = value.map { FfiConverterUByte.allocationSize(it) }.sum()
return sizeForLength + sizeForItems
}
override fun write(value: List<UByte>, buf: ByteBuffer) {
buf.putInt(value.size)
value.forEach {
FfiConverterUByte.write(it, buf)
}
}
}
public object FfiConverterSequenceString : FfiConverterRustBuffer<List<String>> {
override fun read(buf: ByteBuffer): List<String> {
val len = buf.getInt()
return List<String>(len) {
FfiConverterString.read(buf)
}
}
override fun allocationSize(value: List<String>): Int {
val sizeForLength = 4
val sizeForItems = value.map { FfiConverterString.allocationSize(it) }.sum()
return sizeForLength + sizeForItems
}
override fun write(value: List<String>, buf: ByteBuffer) {
buf.putInt(value.size)
value.forEach {
FfiConverterString.write(it, buf)
}
}
}
package cn.qmai.socket
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment